summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/OFL.txt7
-rw-r--r--app/assets/fonts/SourceSansPro-Black.ttfbin289364 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Black.ttf.woffbin0 -> 113800 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Black.ttf.woff2bin0 -> 82052 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttfbin103404 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttf.woffbin0 -> 49704 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2bin0 -> 34812 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-BlackItalic.ttfbin116360 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Bold.ttfbin291424 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Bold.ttf.woffbin0 -> 117872 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Bold.ttf.woff2bin0 -> 85604 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttfbin103608 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttf.woffbin0 -> 50608 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2bin0 -> 35864 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-BoldItalic.ttfbin116192 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLight.ttfbin291652 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLight.ttf.woffbin0 -> 114336 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2bin0 -> 82808 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttfbin104768 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woffbin0 -> 49684 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2bin0 -> 34560 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-ExtraLightItalic.ttfbin117140 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttfbin104236 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttf.woffbin0 -> 51012 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttf.woff2bin0 -> 36016 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Italic.ttfbin117328 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Light.ttfbin293220 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Light.ttf.woffbin0 -> 118284 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Light.ttf.woff2bin0 -> 86336 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttfbin104616 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttf.woffbin0 -> 50992 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttf.woff2bin0 -> 35952 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-LightItalic.ttfbin116960 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Regular.ttfbin293956 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Regular.ttf.woffbin0 -> 119064 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Regular.ttf.woff2bin0 -> 86844 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Semibold.ttfbin292404 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Semibold.ttf.woffbin0 -> 118412 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Semibold.ttf.woff2bin0 -> 86196 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttfbin104020 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woffbin0 -> 50924 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2bin0 -> 35984 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-SemiboldItalic.ttfbin116424 -> 0 bytes
-rw-r--r--app/assets/images/auth_buttons/azure_64.pngbin0 -> 986 bytes
-rw-r--r--app/assets/images/emoji.pngbin832902 -> 263533 bytes
-rw-r--r--app/assets/images/emoji@2x.pngbin0 -> 690504 bytes
-rw-r--r--app/assets/javascripts/activities.js.coffee18
-rw-r--r--app/assets/javascripts/admin.js.coffee15
-rw-r--r--app/assets/javascripts/api.js.coffee17
-rw-r--r--app/assets/javascripts/application.js.coffee92
-rw-r--r--app/assets/javascripts/autosave.js.coffee6
-rw-r--r--app/assets/javascripts/awards_handler.coffee95
-rw-r--r--app/assets/javascripts/behaviors/autosize.js.coffee22
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js.coffee47
-rw-r--r--app/assets/javascripts/blob/edit_blob.js.coffee1
-rw-r--r--app/assets/javascripts/branch-graph.js.coffee2
-rw-r--r--app/assets/javascripts/breakpoints.coffee37
-rw-r--r--app/assets/javascripts/broadcast_message.js.coffee22
-rw-r--r--app/assets/javascripts/build_artifacts.js.coffee14
-rw-r--r--app/assets/javascripts/calendar.js.coffee5
-rw-r--r--app/assets/javascripts/ci/build.coffee13
-rw-r--r--app/assets/javascripts/commits.js.coffee66
-rw-r--r--app/assets/javascripts/dashboard.js.coffee3
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee25
-rw-r--r--app/assets/javascripts/dropzone_input.js.coffee16
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.coffee8
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee276
-rw-r--r--app/assets/javascripts/issuable_context.js.coffee16
-rw-r--r--app/assets/javascripts/issue.js.coffee29
-rw-r--r--app/assets/javascripts/issue_status_select.js.coffee11
-rw-r--r--app/assets/javascripts/issues.js.coffee33
-rw-r--r--app/assets/javascripts/labels_select.js.coffee109
-rw-r--r--app/assets/javascripts/logo.js.coffee50
-rw-r--r--app/assets/javascripts/markdown_preview.js.coffee46
-rw-r--r--app/assets/javascripts/merge_request.js.coffee39
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee22
-rw-r--r--app/assets/javascripts/merge_requests.js.coffee4
-rw-r--r--app/assets/javascripts/milestone.js.coffee23
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee74
-rw-r--r--app/assets/javascripts/notes.js.coffee234
-rw-r--r--app/assets/javascripts/pager.js.coffee3
-rw-r--r--app/assets/javascripts/profile.js.coffee16
-rw-r--r--app/assets/javascripts/project.js.coffee16
-rw-r--r--app/assets/javascripts/project_find_file.js.coffee125
-rw-r--r--app/assets/javascripts/project_new.js.coffee13
-rw-r--r--app/assets/javascripts/project_select.js.coffee3
-rw-r--r--app/assets/javascripts/projects_list.js.coffee53
-rw-r--r--app/assets/javascripts/shortcuts.js.coffee20
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js.coffee19
-rw-r--r--app/assets/javascripts/shortcuts_issuable.coffee31
-rw-r--r--app/assets/javascripts/sidebar.js.coffee24
-rw-r--r--app/assets/javascripts/star.js.coffee4
-rw-r--r--app/assets/javascripts/subscription.js.coffee34
-rw-r--r--app/assets/javascripts/todos.js.coffee56
-rw-r--r--app/assets/javascripts/user.js.coffee11
-rw-r--r--app/assets/javascripts/user_tabs.js.coffee146
-rw-r--r--app/assets/javascripts/users_select.js.coffee88
-rw-r--r--app/assets/javascripts/wikis.js.coffee24
-rw-r--r--app/assets/javascripts/zen_mode.js.coffee104
-rw-r--r--app/assets/stylesheets/application.scss6
-rw-r--r--app/assets/stylesheets/framework.scss3
-rw-r--r--app/assets/stylesheets/framework/avatar.scss4
-rw-r--r--app/assets/stylesheets/framework/blocks.scss50
-rw-r--r--app/assets/stylesheets/framework/buttons.scss125
-rw-r--r--app/assets/stylesheets/framework/calendar.scss42
-rw-r--r--app/assets/stylesheets/framework/callout.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss154
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss348
-rw-r--r--app/assets/stylesheets/framework/files.scss62
-rw-r--r--app/assets/stylesheets/framework/filters.scss27
-rw-r--r--app/assets/stylesheets/framework/flash.scss2
-rw-r--r--app/assets/stylesheets/framework/fonts.scss24
-rw-r--r--app/assets/stylesheets/framework/forms.scss52
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss30
-rw-r--r--app/assets/stylesheets/framework/header.scss49
-rw-r--r--app/assets/stylesheets/framework/highlight.scss26
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss20
-rw-r--r--app/assets/stylesheets/framework/jquery.scss33
-rw-r--r--app/assets/stylesheets/framework/layout.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss35
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss29
-rw-r--r--app/assets/stylesheets/framework/mixins.scss43
-rw-r--r--app/assets/stylesheets/framework/mobile.scss17
-rw-r--r--app/assets/stylesheets/framework/nav.scss142
-rw-r--r--app/assets/stylesheets/framework/pagination.scss28
-rw-r--r--app/assets/stylesheets/framework/panels.scss12
-rw-r--r--app/assets/stylesheets/framework/progress.scss5
-rw-r--r--app/assets/stylesheets/framework/selects.scss149
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss95
-rw-r--r--app/assets/stylesheets/framework/tables.scss13
-rw-r--r--app/assets/stylesheets/framework/timeline.scss6
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss66
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss36
-rw-r--r--app/assets/stylesheets/framework/typography.scss46
-rw-r--r--app/assets/stylesheets/framework/variables.scss144
-rw-r--r--app/assets/stylesheets/framework/zen.scss103
-rw-r--r--app/assets/stylesheets/highlight/dark.scss56
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss40
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss40
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss40
-rw-r--r--app/assets/stylesheets/highlight/white.scss140
-rw-r--r--app/assets/stylesheets/pages/admin.scss10
-rw-r--r--app/assets/stylesheets/pages/appearances.scss11
-rw-r--r--app/assets/stylesheets/pages/awards.scss210
-rw-r--r--app/assets/stylesheets/pages/builds.scss25
-rw-r--r--app/assets/stylesheets/pages/commit.scss5
-rw-r--r--app/assets/stylesheets/pages/commits.scss68
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss10
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss13
-rw-r--r--app/assets/stylesheets/pages/diff.scss127
-rw-r--r--app/assets/stylesheets/pages/editor.scss4
-rw-r--r--app/assets/stylesheets/pages/emojis.scss3002
-rw-r--r--app/assets/stylesheets/pages/events.scss24
-rw-r--r--app/assets/stylesheets/pages/explore.scss8
-rw-r--r--app/assets/stylesheets/pages/graph.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss28
-rw-r--r--app/assets/stylesheets/pages/import.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss187
-rw-r--r--app/assets/stylesheets/pages/issues.scss51
-rw-r--r--app/assets/stylesheets/pages/labels.scss28
-rw-r--r--app/assets/stylesheets/pages/login.scss14
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss51
-rw-r--r--app/assets/stylesheets/pages/milestone.scss53
-rw-r--r--app/assets/stylesheets/pages/note_form.scss15
-rw-r--r--app/assets/stylesheets/pages/notes.scss48
-rw-r--r--app/assets/stylesheets/pages/notifications.scss18
-rw-r--r--app/assets/stylesheets/pages/profile.scss152
-rw-r--r--app/assets/stylesheets/pages/projects.scss384
-rw-r--r--app/assets/stylesheets/pages/runners.scss2
-rw-r--r--app/assets/stylesheets/pages/search.scss12
-rw-r--r--app/assets/stylesheets/pages/sherlock.scss4
-rw-r--r--app/assets/stylesheets/pages/snippets.scss34
-rw-r--r--app/assets/stylesheets/pages/status.scss2
-rw-r--r--app/assets/stylesheets/pages/todos.scss96
-rw-r--r--app/assets/stylesheets/pages/tree.scss32
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss11
-rw-r--r--app/assets/stylesheets/pages/wiki.scss5
-rw-r--r--app/assets/stylesheets/pages/xterm.scss52
-rw-r--r--app/controllers/abuse_reports_controller.rb12
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb6
-rw-r--r--app/controllers/admin/appearances_controller.rb57
-rw-r--r--app/controllers/admin/application_settings_controller.rb8
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb37
-rw-r--r--app/controllers/admin/builds_controller.rb6
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/identities_controller.rb2
-rw-r--r--app/controllers/admin/labels_controller.rb2
-rw-r--r--app/controllers/admin/spam_logs_controller.rb17
-rw-r--r--app/controllers/admin/users_controller.rb10
-rw-r--r--app/controllers/application_controller.rb53
-rw-r--r--app/controllers/ci/application_controller.rb47
-rw-r--r--app/controllers/ci/lints_controller.rb6
-rw-r--r--app/controllers/ci/projects_controller.rb13
-rw-r--r--app/controllers/concerns/continue_params.rb13
-rw-r--r--app/controllers/concerns/creates_commit.rb53
-rw-r--r--app/controllers/concerns/filter_projects.rb15
-rw-r--r--app/controllers/concerns/issues_action.rb4
-rw-r--r--app/controllers/concerns/merge_requests_action.rb4
-rw-r--r--app/controllers/concerns/toggle_subscription_action.rb17
-rw-r--r--app/controllers/dashboard/projects_controller.rb25
-rw-r--r--app/controllers/dashboard/todos_controller.rb44
-rw-r--r--app/controllers/dashboard_controller.rb6
-rw-r--r--app/controllers/emojis_controller.rb6
-rw-r--r--app/controllers/explore/groups_controller.rb2
-rw-r--r--app/controllers/explore/projects_controller.rb45
-rw-r--r--app/controllers/groups_controller.rb44
-rw-r--r--app/controllers/oauth/applications_controller.rb24
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb74
-rw-r--r--app/controllers/passwords_controller.rb8
-rw-r--r--app/controllers/profiles/keys_controller.rb8
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb12
-rw-r--r--app/controllers/profiles_controller.rb7
-rw-r--r--app/controllers/projects/application_controller.rb5
-rw-r--r--app/controllers/projects/artifacts_controller.rb46
-rw-r--r--app/controllers/projects/avatars_controller.rb17
-rw-r--r--app/controllers/projects/badges_controller.rb13
-rw-r--r--app/controllers/projects/blame_controller.rb24
-rw-r--r--app/controllers/projects/blob_controller.rb13
-rw-r--r--app/controllers/projects/branches_controller.rb24
-rw-r--r--app/controllers/projects/builds_controller.rb59
-rw-r--r--app/controllers/projects/commit_controller.rb66
-rw-r--r--app/controllers/projects/commits_controller.rb13
-rw-r--r--app/controllers/projects/compare_controller.rb38
-rw-r--r--app/controllers/projects/find_file_controller.rb26
-rw-r--r--app/controllers/projects/forks_controller.rb40
-rw-r--r--app/controllers/projects/group_links_controller.rb23
-rw-r--r--app/controllers/projects/imports_controller.rb34
-rw-r--r--app/controllers/projects/issues_controller.rb25
-rw-r--r--app/controllers/projects/labels_controller.rb11
-rw-r--r--app/controllers/projects/merge_requests_controller.rb31
-rw-r--r--app/controllers/projects/milestones_controller.rb14
-rw-r--r--app/controllers/projects/notes_controller.rb24
-rw-r--r--app/controllers/projects/project_members_controller.rb1
-rw-r--r--app/controllers/projects/raw_controller.rb28
-rw-r--r--app/controllers/projects/refs_controller.rb8
-rw-r--r--app/controllers/projects/repositories_controller.rb4
-rw-r--r--app/controllers/projects/runner_projects_controller.rb2
-rw-r--r--app/controllers/projects/runners_controller.rb2
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/projects/tags_controller.rb7
-rw-r--r--app/controllers/projects/triggers_controller.rb2
-rw-r--r--app/controllers/projects/variables_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb40
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/controllers/sent_notifications_controller.rb25
-rw-r--r--app/controllers/sessions_controller.rb20
-rw-r--r--app/controllers/uploads_controller.rb5
-rw-r--r--app/controllers/users_controller.rb69
-rw-r--r--app/finders/groups_finder.rb44
-rw-r--r--app/finders/issuable_finder.rb38
-rw-r--r--app/finders/issues_finder.rb6
-rw-r--r--app/finders/joined_groups_finder.rb49
-rw-r--r--app/finders/projects_finder.rb23
-rw-r--r--app/finders/snippets_finder.rb6
-rw-r--r--app/finders/todos_finder.rb129
-rw-r--r--app/helpers/appearances_helper.rb28
-rw-r--r--app/helpers/application_helper.rb53
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/auth_helper.rb6
-rw-r--r--app/helpers/blob_helper.rb86
-rw-r--r--app/helpers/broadcast_messages_helper.rb34
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb14
-rw-r--r--app/helpers/commits_helper.rb46
-rw-r--r--app/helpers/diff_helper.rb112
-rw-r--r--app/helpers/dropdowns_helper.rb100
-rw-r--r--app/helpers/events_helper.rb20
-rw-r--r--app/helpers/explore_helper.rb9
-rw-r--r--app/helpers/gitlab_markdown_helper.rb19
-rw-r--r--app/helpers/icons_helper.rb13
-rw-r--r--app/helpers/issuables_helper.rb58
-rw-r--r--app/helpers/issues_helper.rb23
-rw-r--r--app/helpers/labels_helper.rb42
-rw-r--r--app/helpers/milestones_helper.rb39
-rw-r--r--app/helpers/nav_helper.rb14
-rw-r--r--app/helpers/notes_helper.rb2
-rw-r--r--app/helpers/page_layout_helper.rb25
-rw-r--r--app/helpers/projects_helper.rb43
-rw-r--r--app/helpers/search_helper.rb6
-rw-r--r--app/helpers/snippets_helper.rb85
-rw-r--r--app/helpers/sorting_helper.rb36
-rw-r--r--app/helpers/todos_helper.rb87
-rw-r--r--app/helpers/tree_helper.rb3
-rw-r--r--app/mailers/abuse_report_mailer.rb10
-rw-r--r--app/mailers/email_rejection_mailer.rb2
-rw-r--r--app/mailers/emails/builds.rb14
-rw-r--r--app/mailers/emails/issues.rb58
-rw-r--r--app/mailers/emails/merge_requests.rb88
-rw-r--r--app/mailers/emails/notes.rb44
-rw-r--r--app/mailers/emails/profile.rb5
-rw-r--r--app/mailers/emails/projects.rb6
-rw-r--r--app/mailers/notify.rb20
-rw-r--r--app/models/ability.rb140
-rw-r--r--app/models/abuse_report.rb13
-rw-r--r--app/models/appearance.rb9
-rw-r--r--app/models/application_setting.rb38
-rw-r--r--app/models/blob.rb37
-rw-r--r--app/models/broadcast_message.rb20
-rw-r--r--app/models/ci/build.rb110
-rw-r--r--app/models/ci/commit.rb40
-rw-r--r--app/models/ci/runner.rb26
-rw-r--r--app/models/ci/runner_project.rb11
-rw-r--r--app/models/ci/trigger.rb17
-rw-r--r--app/models/ci/variable.rb9
-rw-r--r--app/models/commit.rb60
-rw-r--r--app/models/commit_range.rb4
-rw-r--r--app/models/commit_status.rb87
-rw-r--r--app/models/concerns/issuable.rb82
-rw-r--r--app/models/concerns/mentionable.rb7
-rw-r--r--app/models/concerns/milestoneish.rb29
-rw-r--r--app/models/concerns/sortable.rb3
-rw-r--r--app/models/concerns/subscribable.rb44
-rw-r--r--app/models/diff_line.rb3
-rw-r--r--app/models/email.rb2
-rw-r--r--app/models/event.rb18
-rw-r--r--app/models/external_issue.rb2
-rw-r--r--app/models/generic_commit_status.rb1
-rw-r--r--app/models/global_label.rb7
-rw-r--r--app/models/global_milestone.rb59
-rw-r--r--app/models/group.rb25
-rw-r--r--app/models/hooks/project_hook.rb5
-rw-r--r--app/models/hooks/service_hook.rb5
-rw-r--r--app/models/hooks/system_hook.rb5
-rw-r--r--app/models/hooks/web_hook.rb11
-rw-r--r--app/models/identity.rb4
-rw-r--r--app/models/issue.rb45
-rw-r--r--app/models/key.rb3
-rw-r--r--app/models/label.rb70
-rw-r--r--app/models/member.rb8
-rw-r--r--app/models/members/project_member.rb6
-rw-r--r--app/models/merge_request.rb167
-rw-r--r--app/models/merge_request_diff.rb111
-rw-r--r--app/models/milestone.rb68
-rw-r--r--app/models/namespace.rb13
-rw-r--r--app/models/note.rb102
-rw-r--r--app/models/personal_snippet.rb1
-rw-r--r--app/models/project.rb157
-rw-r--r--app/models/project_group_link.rb36
-rw-r--r--app/models/project_services/asana_service.rb84
-rw-r--r--app/models/project_services/assembla_service.rb1
-rw-r--r--app/models/project_services/bamboo_service.rb1
-rw-r--r--app/models/project_services/buildkite_service.rb1
-rw-r--r--app/models/project_services/builds_email_service.rb7
-rw-r--r--app/models/project_services/campfire_service.rb1
-rw-r--r--app/models/project_services/ci_service.rb11
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb1
-rw-r--r--app/models/project_services/drone_ci_service.rb1
-rw-r--r--app/models/project_services/emails_on_push_service.rb1
-rw-r--r--app/models/project_services/external_wiki_service.rb1
-rw-r--r--app/models/project_services/flowdock_service.rb1
-rw-r--r--app/models/project_services/gemnasium_service.rb1
-rw-r--r--app/models/project_services/gitlab_ci_service.rb1
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb5
-rw-r--r--app/models/project_services/hipchat_service.rb9
-rw-r--r--app/models/project_services/irker_service.rb8
-rw-r--r--app/models/project_services/issue_tracker_service.rb7
-rw-r--r--app/models/project_services/jira_service.rb17
-rw-r--r--app/models/project_services/pivotaltracker_service.rb1
-rw-r--r--app/models/project_services/pushover_service.rb3
-rw-r--r--app/models/project_services/redmine_service.rb1
-rw-r--r--app/models/project_services/slack_service.rb1
-rw-r--r--app/models/project_services/teamcity_service.rb1
-rw-r--r--app/models/project_snippet.rb3
-rw-r--r--app/models/project_team.rb54
-rw-r--r--app/models/project_wiki.rb15
-rw-r--r--app/models/repository.rb285
-rw-r--r--app/models/sent_notification.rb12
-rw-r--r--app/models/service.rb12
-rw-r--r--app/models/snippet.rb31
-rw-r--r--app/models/spam_log.rb10
-rw-r--r--app/models/spam_report.rb5
-rw-r--r--app/models/subscription.rb2
-rw-r--r--app/models/todo.rb53
-rw-r--r--app/models/tree.rb22
-rw-r--r--app/models/user.rb198
-rw-r--r--app/models/wiki_page.rb6
-rw-r--r--app/services/archive_repository_service.rb23
-rw-r--r--app/services/base_service.rb4
-rw-r--r--app/services/ci/create_builds_service.rb1
-rw-r--r--app/services/ci/image_for_build_service.rb21
-rw-r--r--app/services/commits/revert_service.rb59
-rw-r--r--app/services/compare_service.rb12
-rw-r--r--app/services/create_branch_service.rb7
-rw-r--r--app/services/create_commit_builds_service.rb1
-rw-r--r--app/services/create_spam_log_service.rb13
-rw-r--r--app/services/create_tag_service.rb1
-rw-r--r--app/services/delete_branch_service.rb7
-rw-r--r--app/services/delete_user_service.rb24
-rw-r--r--app/services/destroy_group_service.rb6
-rw-r--r--app/services/git_push_service.rb142
-rw-r--r--app/services/git_tag_push_service.rb2
-rw-r--r--app/services/issuable_base_service.rb26
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/issues/create_service.rb1
-rw-r--r--app/services/issues/update_service.rb17
-rw-r--r--app/services/merge_requests/build_service.rb37
-rw-r--r--app/services/merge_requests/close_service.rb1
-rw-r--r--app/services/merge_requests/create_service.rb3
-rw-r--r--app/services/merge_requests/merge_service.rb5
-rw-r--r--app/services/merge_requests/merge_when_build_succeeds_service.rb27
-rw-r--r--app/services/merge_requests/post_merge_service.rb4
-rw-r--r--app/services/merge_requests/update_service.rb21
-rw-r--r--app/services/notes/create_service.rb21
-rw-r--r--app/services/notes/post_process_service.rb28
-rw-r--r--app/services/notes/update_service.rb4
-rw-r--r--app/services/notification_service.rb90
-rw-r--r--app/services/projects/autocomplete_service.rb6
-rw-r--r--app/services/projects/destroy_service.rb18
-rw-r--r--app/services/projects/download_service.rb8
-rw-r--r--app/services/projects/housekeeping_service.rb47
-rw-r--r--app/services/projects/import_service.rb67
-rw-r--r--app/services/projects/transfer_service.rb3
-rw-r--r--app/services/projects/upload_service.rb8
-rw-r--r--app/services/repair_ldap_blocked_user_service.rb17
-rw-r--r--app/services/search/global_service.rb3
-rw-r--r--app/services/search/project_service.rb3
-rw-r--r--app/services/search/snippet_service.rb5
-rw-r--r--app/services/system_hooks_service.rb17
-rw-r--r--app/services/system_note_service.rb37
-rw-r--r--app/services/todo_service.rb170
-rw-r--r--app/uploaders/artifact_uploader.rb4
-rw-r--r--app/uploaders/file_uploader.rb15
-rw-r--r--app/validators/email_validator.rb15
-rw-r--r--app/validators/namespace_validator.rb1
-rw-r--r--app/validators/url_validator.rb3
-rw-r--r--app/views/abuse_report_mailer/notify.html.haml2
-rw-r--r--app/views/abuse_reports/new.html.haml4
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml23
-rw-r--r--app/views/admin/abuse_reports/index.html.haml3
-rw-r--r--app/views/admin/appearances/_form.html.haml58
-rw-r--r--app/views/admin/appearances/preview.html.haml29
-rw-r--r--app/views/admin/appearances/show.html.haml7
-rw-r--r--app/views/admin/application_settings/_form.html.haml77
-rw-r--r--app/views/admin/applications/_form.html.haml2
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml40
-rw-r--r--app/views/admin/broadcast_messages/edit.html.haml3
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml81
-rw-r--r--app/views/admin/broadcast_messages/preview.js.haml1
-rw-r--r--app/views/admin/builds/_build.html.haml25
-rw-r--r--app/views/admin/builds/index.html.haml27
-rw-r--r--app/views/admin/dashboard/index.html.haml27
-rw-r--r--app/views/admin/deploy_keys/index.html.haml2
-rw-r--r--app/views/admin/groups/_form.html.haml3
-rw-r--r--app/views/admin/groups/index.html.haml4
-rw-r--r--app/views/admin/groups/show.html.haml18
-rw-r--r--app/views/admin/hooks/index.html.haml3
-rw-r--r--app/views/admin/labels/_form.html.haml4
-rw-r--r--app/views/admin/labels/_label.html.haml10
-rw-r--r--app/views/admin/labels/index.html.haml2
-rw-r--r--app/views/admin/logs/show.html.haml5
-rw-r--r--app/views/admin/projects/index.html.haml6
-rw-r--r--app/views/admin/projects/show.html.haml4
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml32
-rw-r--r--app/views/admin/spam_logs/index.html.haml21
-rw-r--r--app/views/admin/users/_form.html.haml8
-rw-r--r--app/views/admin/users/_head.html.haml7
-rw-r--r--app/views/admin/users/_profile.html.haml2
-rw-r--r--app/views/admin/users/index.html.haml196
-rw-r--r--app/views/admin/users/keys.html.haml2
-rw-r--r--app/views/admin/users/show.html.haml28
-rw-r--r--app/views/ci/commits/_commit.html.haml32
-rw-r--r--app/views/ci/lints/show.html.haml6
-rw-r--r--app/views/dashboard/_activities.html.haml4
-rw-r--r--app/views/dashboard/_activity_head.html.haml2
-rw-r--r--app/views/dashboard/_groups_head.html.haml20
-rw-r--r--app/views/dashboard/_projects_head.html.haml14
-rw-r--r--app/views/dashboard/_snippets_head.html.haml2
-rw-r--r--app/views/dashboard/groups/index.html.haml9
-rw-r--r--app/views/dashboard/issues.atom.builder2
-rw-r--r--app/views/dashboard/issues.html.haml19
-rw-r--r--app/views/dashboard/merge_requests.html.haml10
-rw-r--r--app/views/dashboard/milestones/_issue.html.haml10
-rw-r--r--app/views/dashboard/milestones/_issues.html.haml6
-rw-r--r--app/views/dashboard/milestones/_merge_request.html.haml10
-rw-r--r--app/views/dashboard/milestones/_merge_requests.html.haml6
-rw-r--r--app/views/dashboard/milestones/_milestone.html.haml31
-rw-r--r--app/views/dashboard/milestones/index.html.haml9
-rw-r--r--app/views/dashboard/milestones/show.html.haml106
-rw-r--r--app/views/dashboard/projects/_projects.html.haml4
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml8
-rw-r--r--app/views/dashboard/projects/index.atom.builder2
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/dashboard/snippets/index.html.haml48
-rw-r--r--app/views/dashboard/todos/_todo.html.haml26
-rw-r--r--app/views/dashboard/todos/index.html.haml66
-rw-r--r--app/views/devise/sessions/new.html.haml6
-rw-r--r--app/views/devise/shared/_signin_box.html.haml2
-rw-r--r--app/views/doorkeeper/applications/_delete_form.html.haml8
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml31
-rw-r--r--app/views/doorkeeper/applications/index.html.haml98
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml11
-rw-r--r--app/views/emojis/index.html.haml11
-rw-r--r--app/views/events/_commit.html.haml2
-rw-r--r--app/views/events/_event.html.haml6
-rw-r--r--app/views/events/_event_last_push.html.haml4
-rw-r--r--app/views/events/event/_common.html.haml2
-rw-r--r--app/views/events/event/_push.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/_dropdown.html.haml27
-rw-r--r--app/views/explore/projects/_filter.html.haml81
-rw-r--r--app/views/explore/projects/_nav.html.haml10
-rw-r--r--app/views/explore/projects/_projects.html.haml7
-rw-r--r--app/views/explore/projects/index.html.haml9
-rw-r--r--app/views/explore/projects/starred.html.haml11
-rw-r--r--app/views/explore/projects/trending.html.haml10
-rw-r--r--app/views/groups/_activities.html.haml12
-rw-r--r--app/views/groups/_projects.html.haml12
-rw-r--r--app/views/groups/_shared_projects.html.haml1
-rw-r--r--app/views/groups/activity.html.haml9
-rw-r--r--app/views/groups/edit.html.haml11
-rw-r--r--app/views/groups/group_members/_group_member.html.haml3
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml5
-rw-r--r--app/views/groups/issues.atom.builder2
-rw-r--r--app/views/groups/issues.html.haml16
-rw-r--r--app/views/groups/merge_requests.html.haml7
-rw-r--r--app/views/groups/milestones/_issue.html.haml10
-rw-r--r--app/views/groups/milestones/_issues.html.haml6
-rw-r--r--app/views/groups/milestones/_merge_request.html.haml10
-rw-r--r--app/views/groups/milestones/_merge_requests.html.haml6
-rw-r--r--app/views/groups/milestones/_milestone.html.haml34
-rw-r--r--app/views/groups/milestones/index.html.haml16
-rw-r--r--app/views/groups/milestones/new.html.haml6
-rw-r--r--app/views/groups/milestones/show.html.haml114
-rw-r--r--app/views/groups/projects.html.haml6
-rw-r--r--app/views/groups/show.atom.builder2
-rw-r--r--app/views/groups/show.html.haml63
-rw-r--r--app/views/help/_shortcuts.html.haml42
-rw-r--r--app/views/help/ui.html.haml474
-rw-r--r--app/views/kaminari/gitlab/_next_page.html.haml8
-rw-r--r--app/views/kaminari/gitlab/_paginator.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_prev_page.html.haml8
-rw-r--r--app/views/layouts/_broadcast.html.haml5
-rw-r--r--app/views/layouts/_head.html.haml8
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml8
-rw-r--r--app/views/layouts/_page.html.haml7
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/application.html.haml6
-rw-r--r--app/views/layouts/ci/_page.html.haml3
-rw-r--r--app/views/layouts/group.html.haml7
-rw-r--r--app/views/layouts/header/_default.html.haml48
-rw-r--r--app/views/layouts/header/_public.html.haml10
-rw-r--r--app/views/layouts/nav/_admin.html.haml25
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml20
-rw-r--r--app/views/layouts/nav/_group.html.haml11
-rw-r--r--app/views/layouts/nav/_profile.html.haml6
-rw-r--r--app/views/layouts/nav/_project.html.haml18
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml10
-rw-r--r--app/views/layouts/notify.html.haml13
-rw-r--r--app/views/layouts/project.html.haml7
-rw-r--r--app/views/notify/_note_message.html.haml3
-rw-r--r--app/views/notify/_reassigned_issuable_email.text.erb2
-rw-r--r--app/views/notify/_relabeled_issuable_email.html.haml3
-rw-r--r--app/views/notify/_relabeled_issuable_email.text.erb3
-rw-r--r--app/views/notify/build_fail_email.html.haml3
-rw-r--r--app/views/notify/build_success_email.html.haml2
-rw-r--r--app/views/notify/new_issue_email.html.haml3
-rw-r--r--app/views/notify/new_merge_request_email.html.haml3
-rw-r--r--app/views/notify/relabeled_issue_email.html.haml1
-rw-r--r--app/views/notify/relabeled_issue_email.text.erb1
-rw-r--r--app/views/notify/relabeled_merge_request_email.html.haml1
-rw-r--r--app/views/notify/relabeled_merge_request_email.text.erb1
-rw-r--r--app/views/notify/repository_push_email.html.haml4
-rw-r--r--app/views/notify/repository_push_email.text.haml2
-rw-r--r--app/views/profiles/_event_table.html.haml28
-rw-r--r--app/views/profiles/accounts/show.html.haml215
-rw-r--r--app/views/profiles/applications.html.haml70
-rw-r--r--app/views/profiles/audit_log.html.haml13
-rw-r--r--app/views/profiles/emails/index.html.haml93
-rw-r--r--app/views/profiles/keys/_form.html.haml14
-rw-r--r--app/views/profiles/keys/_key.html.haml25
-rw-r--r--app/views/profiles/keys/_key_details.html.haml4
-rw-r--r--app/views/profiles/keys/_key_table.html.haml20
-rw-r--r--app/views/profiles/keys/index.html.haml29
-rw-r--r--app/views/profiles/keys/new.html.haml17
-rw-r--r--app/views/profiles/notifications/_settings.html.haml4
-rw-r--r--app/views/profiles/notifications/show.html.haml123
-rw-r--r--app/views/profiles/passwords/edit.html.haml51
-rw-r--r--app/views/profiles/preferences/show.html.haml99
-rw-r--r--app/views/profiles/show.html.haml161
-rw-r--r--app/views/profiles/two_factor_auths/new.html.haml78
-rw-r--r--app/views/projects/_activity.html.haml4
-rw-r--r--app/views/projects/_builds_settings.html.haml60
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/projects/_find_file_link.html.haml3
-rw-r--r--app/views/projects/_home_panel.html.haml49
-rw-r--r--app/views/projects/_md_preview.html.haml2
-rw-r--r--app/views/projects/_zen.html.haml11
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml8
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml9
-rw-r--r--app/views/projects/artifacts/browse.html.haml22
-rw-r--r--app/views/projects/blame/show.html.haml26
-rw-r--r--app/views/projects/blob/_blob.html.haml11
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/_image.html.haml9
-rw-r--r--app/views/projects/blob/_new_dir.html.haml2
-rw-r--r--app/views/projects/blob/_remove.html.haml2
-rw-r--r--app/views/projects/blob/_text.html.haml9
-rw-r--r--app/views/projects/blob/_upload.html.haml2
-rw-r--r--app/views/projects/blob/diff.html.haml5
-rw-r--r--app/views/projects/blob/edit.html.haml4
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/blob/preview.html.haml14
-rw-r--r--app/views/projects/branches/_branch.html.haml17
-rw-r--r--app/views/projects/branches/destroy.js.haml2
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/builds/index.html.haml37
-rw-r--r--app/views/projects/builds/show.html.haml98
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml11
-rw-r--r--app/views/projects/ci/builds/_build.html.haml76
-rw-r--r--app/views/projects/commit/_builds.html.haml9
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commit/_revert.html.haml31
-rw-r--r--app/views/projects/commit/builds.html.haml3
-rw-r--r--app/views/projects/commit/show.html.haml13
-rw-r--r--app/views/projects/commit_statuses/_commit_status.html.haml79
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/commits/_commit_list.html.haml11
-rw-r--r--app/views/projects/commits/_commits.html.haml10
-rw-r--r--app/views/projects/commits/_head.html.haml6
-rw-r--r--app/views/projects/commits/show.atom.builder4
-rw-r--r--app/views/projects/commits/show.html.haml33
-rw-r--r--app/views/projects/compare/_form.html.haml5
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml15
-rw-r--r--app/views/projects/diffs/_file.html.haml64
-rw-r--r--app/views/projects/diffs/_image.html.haml16
-rw-r--r--app/views/projects/diffs/_match_line.html.haml2
-rw-r--r--app/views/projects/diffs/_match_line_parallel.html.haml8
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml52
-rw-r--r--app/views/projects/diffs/_text_file.html.haml24
-rw-r--r--app/views/projects/diffs/_warning.html.haml13
-rw-r--r--app/views/projects/edit.html.haml74
-rw-r--r--app/views/projects/empty.html.haml73
-rw-r--r--app/views/projects/find_file/show.html.haml27
-rw-r--r--app/views/projects/forks/_projects.html.haml2
-rw-r--r--app/views/projects/forks/index.html.haml48
-rw-r--r--app/views/projects/forks/new.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml58
-rw-r--r--app/views/projects/go_import.html.haml5
-rw-r--r--app/views/projects/graphs/_head.html.haml2
-rw-r--r--app/views/projects/group_links/index.html.haml41
-rw-r--r--app/views/projects/hooks/index.html.haml14
-rw-r--r--app/views/projects/issues/_closed_by_box.html.haml6
-rw-r--r--app/views/projects/issues/_discussion.html.haml6
-rw-r--r--app/views/projects/issues/_form.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml14
-rw-r--r--app/views/projects/issues/_issues.html.haml5
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml8
-rw-r--r--app/views/projects/issues/_new_branch.html.haml5
-rw-r--r--app/views/projects/issues/_related_branches.html.haml15
-rw-r--r--app/views/projects/issues/index.atom.builder2
-rw-r--r--app/views/projects/issues/index.html.haml21
-rw-r--r--app/views/projects/issues/show.html.haml78
-rw-r--r--app/views/projects/issues/update.js.haml6
-rw-r--r--app/views/projects/labels/_form.html.haml8
-rw-r--r--app/views/projects/labels/_label.html.haml17
-rw-r--r--app/views/projects/labels/index.html.haml13
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml4
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml17
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml5
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml29
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml12
-rw-r--r--app/views/projects/merge_requests/_show.html.haml21
-rw-r--r--app/views/projects/merge_requests/index.html.haml20
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml3
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml36
-rw-r--r--app/views/projects/merge_requests/update.js.haml6
-rw-r--r--app/views/projects/merge_requests/widget/_merged.html.haml14
-rw-r--r--app/views/projects/merge_requests/widget/_merged_buttons.haml11
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml2
-rw-r--r--app/views/projects/milestones/_form.html.haml6
-rw-r--r--app/views/projects/milestones/_issue.html.haml9
-rw-r--r--app/views/projects/milestones/_issues.html.haml6
-rw-r--r--app/views/projects/milestones/_merge_request.html.haml8
-rw-r--r--app/views/projects/milestones/_merge_requests.html.haml6
-rw-r--r--app/views/projects/milestones/_milestone.html.haml35
-rw-r--r--app/views/projects/milestones/index.html.haml15
-rw-r--r--app/views/projects/milestones/show.html.haml101
-rw-r--r--app/views/projects/notes/_diff_notes_with_reply.html.haml2
-rw-r--r--app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml5
-rw-r--r--app/views/projects/notes/_edit_form.html.haml8
-rw-r--r--app/views/projects/notes/_form.html.haml9
-rw-r--r--app/views/projects/notes/_note.html.haml12
-rw-r--r--app/views/projects/notes/_notes.html.haml4
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml10
-rw-r--r--app/views/projects/notes/discussions/_commit.html.haml3
-rw-r--r--app/views/projects/notes/discussions/_diff.html.haml14
-rw-r--r--app/views/projects/project_members/_group_members.html.haml2
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml2
-rw-r--r--app/views/projects/project_members/_shared_group_members.html.haml21
-rw-r--r--app/views/projects/project_members/_team.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml8
-rw-r--r--app/views/projects/refs/logs_tree.js.haml7
-rw-r--r--app/views/projects/releases/edit.html.haml4
-rw-r--r--app/views/projects/repositories/_download_archive.html.haml2
-rw-r--r--app/views/projects/runners/index.html.haml3
-rw-r--r--app/views/projects/show.atom.builder2
-rw-r--r--app/views/projects/show.html.haml34
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/destroy.js.haml3
-rw-r--r--app/views/projects/tags/new.html.haml4
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--app/views/projects/tree/_tree_content.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml6
-rw-r--r--app/views/projects/tree/show.html.haml10
-rw-r--r--app/views/projects/variables/show.html.haml4
-rw-r--r--app/views/projects/wikis/_form.html.haml4
-rw-r--r--app/views/projects/wikis/_main_links.html.haml23
-rw-r--r--app/views/projects/wikis/_nav.html.haml20
-rw-r--r--app/views/projects/wikis/_new.html.haml16
-rw-r--r--app/views/projects/wikis/edit.html.haml22
-rw-r--r--app/views/projects/wikis/git_access.html.haml12
-rw-r--r--app/views/projects/wikis/history.html.haml13
-rw-r--r--app/views/projects/wikis/pages.html.haml13
-rw-r--r--app/views/projects/wikis/show.html.haml11
-rw-r--r--app/views/search/_category.html.haml2
-rw-r--r--app/views/search/_filter.html.haml46
-rw-r--r--app/views/search/_form.html.haml2
-rw-r--r--app/views/search/_results.html.haml4
-rw-r--r--app/views/search/results/_issue.html.haml1
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml74
-rw-r--r--app/views/search/results/_wiki_blob.html.haml4
-rw-r--r--app/views/search/show.html.haml4
-rw-r--r--app/views/shared/_clone_panel.html.haml4
-rw-r--r--app/views/shared/_commit_message_container.html.haml2
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/views/shared/_file_highlight.html.haml9
-rw-r--r--app/views/shared/_import_form.html.haml2
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/_label_row.html.haml4
-rw-r--r--app/views/shared/_logo.svg28
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/_milestones_filter.html.haml21
-rw-r--r--app/views/shared/_new_project_item_select.html.haml8
-rw-r--r--app/views/shared/_no_ssh.html.haml2
-rw-r--r--app/views/shared/_project_limit.html.haml2
-rw-r--r--app/views/shared/_promo.html.haml6
-rw-r--r--app/views/shared/_service_settings.html.haml4
-rw-r--r--app/views/shared/_sort_dropdown.html.haml6
-rw-r--r--app/views/shared/groups/_group.html.haml26
-rw-r--r--app/views/shared/groups/_list.html.haml6
-rw-r--r--app/views/shared/issuable/_filter.html.haml120
-rw-r--r--app/views/shared/issuable/_form.html.haml13
-rw-r--r--app/views/shared/issuable/_nav.html.haml25
-rw-r--r--app/views/shared/issuable/_participants.html.haml9
-rw-r--r--app/views/shared/issuable/_search_form.html.haml17
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml185
-rw-r--r--app/views/shared/milestones/_issuable.html.haml27
-rw-r--r--app/views/shared/milestones/_issuables.html.haml16
-rw-r--r--app/views/shared/milestones/_issues_tab.html.haml10
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml18
-rw-r--r--app/views/shared/milestones/_merge_requests_tab.haml12
-rw-r--r--app/views/shared/milestones/_milestone.html.haml45
-rw-r--r--app/views/shared/milestones/_participants_tab.html.haml8
-rw-r--r--app/views/shared/milestones/_summary.html.haml28
-rw-r--r--app/views/shared/milestones/_tabs.html.haml30
-rw-r--r--app/views/shared/milestones/_top.html.haml58
-rw-r--r--app/views/shared/projects/_dropdown.html.haml22
-rw-r--r--app/views/shared/projects/_list.html.haml29
-rw-r--r--app/views/shared/projects/_project.html.haml44
-rw-r--r--app/views/shared/snippets/_blob.html.haml5
-rw-r--r--app/views/shared/snippets/_snippet.html.haml7
-rw-r--r--app/views/sherlock/queries/show.html.haml2
-rw-r--r--app/views/sherlock/transactions/_queries.html.haml2
-rw-r--r--app/views/sherlock/transactions/show.html.haml2
-rw-r--r--app/views/snippets/_snippets.html.haml2
-rw-r--r--app/views/users/show.atom.builder2
-rw-r--r--app/views/users/show.html.haml211
-rw-r--r--app/views/votes/_votes_block.html.haml46
-rw-r--r--app/workers/delete_user_worker.rb10
-rw-r--r--app/workers/irker_worker.rb2
-rw-r--r--app/workers/new_note_worker.rb12
-rw-r--r--app/workers/post_receive.rb46
-rw-r--r--app/workers/project_destroy_worker.rb17
-rw-r--r--app/workers/repository_fork_worker.rb1
-rw-r--r--app/workers/repository_import_worker.rb47
784 files changed, 15210 insertions, 8187 deletions
diff --git a/app/assets/fonts/OFL.txt b/app/assets/fonts/OFL.txt
index a9b845ed1d4..df187637e18 100755..100644
--- a/app/assets/fonts/OFL.txt
+++ b/app/assets/fonts/OFL.txt
@@ -1,7 +1,8 @@
-Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
+Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
-This license is copied below, and is also available with a FAQ at:
-http://scripts.sil.org/OFL
+
+This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf b/app/assets/fonts/SourceSansPro-Black.ttf
deleted file mode 100644
index 9c9b5cb7f03..00000000000
--- a/app/assets/fonts/SourceSansPro-Black.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff b/app/assets/fonts/SourceSansPro-Black.ttf.woff
new file mode 100644
index 00000000000..b7e86200927
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Black.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff2 b/app/assets/fonts/SourceSansPro-Black.ttf.woff2
new file mode 100644
index 00000000000..c90d078406c
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Black.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf b/app/assets/fonts/SourceSansPro-BlackIt.ttf
deleted file mode 100644
index 294ce5abe8f..00000000000
--- a/app/assets/fonts/SourceSansPro-BlackIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff
new file mode 100644
index 00000000000..c3314b1ef06
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2
new file mode 100644
index 00000000000..b87e22c41b5
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackItalic.ttf b/app/assets/fonts/SourceSansPro-BlackItalic.ttf
deleted file mode 100755
index c719243c0d6..00000000000
--- a/app/assets/fonts/SourceSansPro-BlackItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf b/app/assets/fonts/SourceSansPro-Bold.ttf
deleted file mode 100644
index 5d65c93242f..00000000000
--- a/app/assets/fonts/SourceSansPro-Bold.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff b/app/assets/fonts/SourceSansPro-Bold.ttf.woff
new file mode 100644
index 00000000000..d1d40f840f8
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Bold.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2
new file mode 100644
index 00000000000..0f46f3e833a
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf b/app/assets/fonts/SourceSansPro-BoldIt.ttf
deleted file mode 100644
index 3decd130070..00000000000
--- a/app/assets/fonts/SourceSansPro-BoldIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff
new file mode 100644
index 00000000000..ef6ff514d3a
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2
new file mode 100644
index 00000000000..8007df6df32
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldItalic.ttf b/app/assets/fonts/SourceSansPro-BoldItalic.ttf
deleted file mode 100755
index d20dd0c5eca..00000000000
--- a/app/assets/fonts/SourceSansPro-BoldItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf b/app/assets/fonts/SourceSansPro-ExtraLight.ttf
deleted file mode 100644
index 253eafa3783..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff
new file mode 100644
index 00000000000..1e6c94d9eb3
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2
new file mode 100644
index 00000000000..b715f274082
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf
deleted file mode 100644
index 00d7e9a7aa8..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff
new file mode 100644
index 00000000000..7a408b1ec73
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2
new file mode 100644
index 00000000000..d8f9d29d4aa
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf b/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf
deleted file mode 100755
index 2c34f3b8dc4..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf b/app/assets/fonts/SourceSansPro-It.ttf
deleted file mode 100644
index f7af5377595..00000000000
--- a/app/assets/fonts/SourceSansPro-It.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff b/app/assets/fonts/SourceSansPro-It.ttf.woff
new file mode 100644
index 00000000000..4d54bc95718
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-It.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff2 b/app/assets/fonts/SourceSansPro-It.ttf.woff2
new file mode 100644
index 00000000000..a00852641f8
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-It.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Italic.ttf b/app/assets/fonts/SourceSansPro-Italic.ttf
deleted file mode 100755
index e5a1a86e631..00000000000
--- a/app/assets/fonts/SourceSansPro-Italic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf b/app/assets/fonts/SourceSansPro-Light.ttf
deleted file mode 100644
index 83a0a336661..00000000000
--- a/app/assets/fonts/SourceSansPro-Light.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff b/app/assets/fonts/SourceSansPro-Light.ttf.woff
new file mode 100644
index 00000000000..1706d57d3c5
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Light.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff2 b/app/assets/fonts/SourceSansPro-Light.ttf.woff2
new file mode 100644
index 00000000000..d8b610ad76e
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Light.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf b/app/assets/fonts/SourceSansPro-LightIt.ttf
deleted file mode 100644
index f18827985ef..00000000000
--- a/app/assets/fonts/SourceSansPro-LightIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff
new file mode 100644
index 00000000000..87378d6c609
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2
new file mode 100644
index 00000000000..e0eebac8273
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightItalic.ttf b/app/assets/fonts/SourceSansPro-LightItalic.ttf
deleted file mode 100755
index 88a6778d24f..00000000000
--- a/app/assets/fonts/SourceSansPro-LightItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf b/app/assets/fonts/SourceSansPro-Regular.ttf
deleted file mode 100644
index 44486cdc670..00000000000
--- a/app/assets/fonts/SourceSansPro-Regular.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff b/app/assets/fonts/SourceSansPro-Regular.ttf.woff
new file mode 100644
index 00000000000..460ab12a638
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Regular.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2
new file mode 100644
index 00000000000..0dd3464c74b
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf b/app/assets/fonts/SourceSansPro-Semibold.ttf
deleted file mode 100644
index 86b00c067e0..00000000000
--- a/app/assets/fonts/SourceSansPro-Semibold.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff
new file mode 100644
index 00000000000..43379631b2d
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2
new file mode 100644
index 00000000000..2526d2e1b60
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf
deleted file mode 100644
index 13d66a1fc45..00000000000
--- a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff
new file mode 100644
index 00000000000..232c2048ae7
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2
new file mode 100644
index 00000000000..606935af089
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf b/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf
deleted file mode 100755
index 2c5ad3008c3..00000000000
--- a/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/auth_buttons/azure_64.png b/app/assets/images/auth_buttons/azure_64.png
new file mode 100644
index 00000000000..a82c751e001
--- /dev/null
+++ b/app/assets/images/auth_buttons/azure_64.png
Binary files differ
diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png
index a8ad7b6eab6..1e7cf79ea45 100644
--- a/app/assets/images/emoji.png
+++ b/app/assets/images/emoji.png
Binary files differ
diff --git a/app/assets/images/emoji@2x.png b/app/assets/images/emoji@2x.png
new file mode 100644
index 00000000000..74d67f7520d
--- /dev/null
+++ b/app/assets/images/emoji@2x.png
Binary files differ
diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee
index 63803747413..5092e824e65 100644
--- a/app/assets/javascripts/activities.js.coffee
+++ b/app/assets/javascripts/activities.js.coffee
@@ -1,7 +1,7 @@
class @Activities
constructor: ->
Pager.init 20, true
- $(".event-filter .btn").bind "click", (event) =>
+ $(".event-filter-link").on "click", (event) =>
event.preventDefault()
@toggleFilter($(event.currentTarget))
@reloadActivities()
@@ -12,18 +12,10 @@ class @Activities
toggleFilter: (sender) ->
- sender.toggleClass "active"
+ $('.event-filter .active').removeClass "active"
event_filters = $.cookie("event_filter")
filter = sender.attr("id").split("_")[0]
- if event_filters
- event_filters = event_filters.split(",")
- else
- event_filters = new Array()
+ $.cookie "event_filter", (if event_filters isnt filter then filter else ""), { path: '/' }
- index = event_filters.indexOf(filter)
- if index is -1
- event_filters.push filter
- else
- event_filters.splice index, 1
-
- $.cookie "event_filter", event_filters.join(","), { path: '/' }
+ if event_filters isnt filter
+ sender.closest('li').toggleClass "active"
diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee
index bcb2e6df7c0..b2b8e1b7ffb 100644
--- a/app/assets/javascripts/admin.js.coffee
+++ b/app/assets/javascripts/admin.js.coffee
@@ -10,20 +10,7 @@ class @Admin
$('body').on 'click', '.js-toggle-colors-link', (e) ->
e.preventDefault()
- $('.js-toggle-colors-link').hide()
- $('.js-toggle-colors-container').show()
-
- $('input#broadcast_message_color').on 'input', ->
- previewColor = $('input#broadcast_message_color').val()
- $('div.broadcast-message-preview').css('background-color', previewColor)
-
- $('input#broadcast_message_font').on 'input', ->
- previewColor = $('input#broadcast_message_font').val()
- $('div.broadcast-message-preview').css('color', previewColor)
-
- $('textarea#broadcast_message_message').on 'input', ->
- previewMessage = $('textarea#broadcast_message_message').val()
- $('div.broadcast-message-preview span').text(previewMessage)
+ $('.js-toggle-colors-container').toggle()
$('.log-tabs a').click (e) ->
e.preventDefault()
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 746fa3cea87..2ddf8612db3 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -4,6 +4,7 @@
namespaces_path: "/api/:version/namespaces.json"
group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json"
+ labels_path: "/api/:version/projects/:id/labels"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
@@ -47,7 +48,7 @@
callback(namespaces)
# Return projects list. Filtered by query
- projects: (query, callback) ->
+ projects: (query, order, callback) ->
url = Api.buildUrl(Api.projects_path)
$.ajax(
@@ -55,11 +56,25 @@
data:
private_token: gon.api_token
search: query
+ order_by: order
per_page: 20
dataType: "json"
).done (projects) ->
callback(projects)
+ newLabel: (project_id, data, callback) ->
+ url = Api.buildUrl(Api.labels_path)
+ url = url.replace(':id', project_id)
+
+ data.private_token = gon.api_token
+ $.ajax(
+ url: url
+ type: "POST"
+ data: data
+ dataType: "json"
+ ).done (label) ->
+ callback(label)
+
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index affab5bb030..d415bbd3476 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -5,32 +5,32 @@
# the compiled file.
#
#= require jquery
-#= require jquery-ui
+#= require jquery-ui/autocomplete
+#= require jquery-ui/datepicker
+#= require jquery-ui/effect-highlight
+#= require jquery-ui/sortable
#= require jquery_ujs
#= require jquery.cookie
#= require jquery.endless-scroll
#= require jquery.highlight
-#= require jquery.history
#= require jquery.waitforimages
#= require jquery.atwho
#= require jquery.scrollTo
-#= require jquery.blockUI
#= require jquery.turbolinks
+#= require d3
+#= require cal-heatmap
#= require turbolinks
#= require autosave
#= require bootstrap
#= require select2
#= require raphael
-#= require g.raphael-min
-#= require g.bar-min
-#= require chart-lib.min
+#= require g.raphael
+#= require g.bar
+#= require Chart
#= require branch-graph
#= require ace/ace
#= require ace/ext-searchbox
-#= require d3
#= require underscore
-#= require nprogress
-#= require nprogress-turbolinks
#= require dropzone
#= require mousetrap
#= require mousetrap/pause
@@ -39,9 +39,9 @@
#= require shortcuts_dashboard_navigation
#= require shortcuts_issuable
#= require shortcuts_network
-#= require cal-heatmap
-#= require jquery.nicescroll.min
+#= require jquery.nicescroll
#= require_tree .
+#= require fuzzaldrin-plus
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
@@ -107,6 +107,8 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
+ bootstrapBreakpoint = bp.getBreakpointSize()
+
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents
@@ -204,4 +206,72 @@ $ ->
form = btn.closest("form")
new ConfirmDangerModal(form, text)
+ $('input[type="search"]').each ->
+ $this = $(this)
+ $this.attr 'value', $this.val()
+ return
+
+ $(document)
+ .off 'keyup', 'input[type="search"]'
+ .on 'keyup', 'input[type="search"]' , (e) ->
+ $this = $(this)
+ $this.attr 'value', $this.val()
+
+ $(document)
+ .off 'breakpoint:change'
+ .on 'breakpoint:change', (e, breakpoint) ->
+ if breakpoint is 'sm' or breakpoint is 'xs'
+ $gutterIcon = $('.js-sidebar-toggle').find('i')
+ if $gutterIcon.hasClass('fa-angle-double-right')
+ $gutterIcon.closest('a').trigger('click')
+
+ $(document)
+ .off 'click', '.js-sidebar-toggle'
+ .on 'click', '.js-sidebar-toggle', (e, triggered) ->
+ e.preventDefault()
+ $this = $(this)
+ $thisIcon = $this.find 'i'
+ $allGutterToggleIcons = $('.js-sidebar-toggle i')
+ if $thisIcon.hasClass('fa-angle-double-right')
+ $allGutterToggleIcons
+ .removeClass('fa-angle-double-right')
+ .addClass('fa-angle-double-left')
+ $('aside.right-sidebar')
+ .removeClass('right-sidebar-expanded')
+ .addClass('right-sidebar-collapsed')
+ $('.page-with-sidebar')
+ .removeClass('right-sidebar-expanded')
+ .addClass('right-sidebar-collapsed')
+ else
+ $allGutterToggleIcons
+ .removeClass('fa-angle-double-left')
+ .addClass('fa-angle-double-right')
+ $('aside.right-sidebar')
+ .removeClass('right-sidebar-collapsed')
+ .addClass('right-sidebar-expanded')
+ $('.page-with-sidebar')
+ .removeClass('right-sidebar-collapsed')
+ .addClass('right-sidebar-expanded')
+ if not triggered
+ $.cookie("collapsed_gutter",
+ $('.right-sidebar')
+ .hasClass('right-sidebar-collapsed'), { path: '/' })
+
+ fitSidebarForSize = ->
+ oldBootstrapBreakpoint = bootstrapBreakpoint
+ bootstrapBreakpoint = bp.getBreakpointSize()
+ if bootstrapBreakpoint != oldBootstrapBreakpoint
+ $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
+
+ checkInitialSidebarSize = ->
+ bootstrapBreakpoint = bp.getBreakpointSize()
+ if bootstrapBreakpoint is "xs" or "sm"
+ $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
+
+ $(window)
+ .off "resize"
+ .on "resize", (e) ->
+ fitSidebarForSize()
+
+ checkInitialSidebarSize()
new Aside()
diff --git a/app/assets/javascripts/autosave.js.coffee b/app/assets/javascripts/autosave.js.coffee
index 5d3fe81da74..28f8e103664 100644
--- a/app/assets/javascripts/autosave.js.coffee
+++ b/app/assets/javascripts/autosave.js.coffee
@@ -16,11 +16,11 @@ class @Autosave
try
text = window.localStorage.getItem @key
- catch
+ catch e
return
@field.val text if text?.length > 0
- @field.trigger "input"
+ @field.trigger "input"
save: ->
return unless window.localStorage?
@@ -35,5 +35,5 @@ class @Autosave
reset: ->
return unless window.localStorage?
- try
+ try
window.localStorage.removeItem @key
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index 619abb1fb07..03a44874161 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -1,25 +1,55 @@
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
- $(".add-award").click (event)->
+ $(".js-add-award").on "click", (event) =>
event.stopPropagation()
event.preventDefault()
- $(".emoji-menu").show()
- $("html").click ->
+ @showEmojiMenu()
+
+ $("html").on 'click', (event) ->
if !$(event.target).closest(".emoji-menu").length
if $(".emoji-menu").is(":visible")
- $(".emoji-menu").hide()
+ $(".emoji-menu").removeClass "is-visible"
+
+ $(".awards")
+ .off "click"
+ .on "click", ".js-emoji-btn", @handleClick
@renderFrequentlyUsedBlock()
- @setupSearch()
+
+ handleClick: (e) ->
+ e.preventDefault()
+ emoji = $(this)
+ .find(".icon")
+ .data "emoji"
+ awards_handler.addAward emoji
+
+ showEmojiMenu: ->
+ if $(".emoji-menu").length
+ if $(".emoji-menu").is ".is-visible"
+ $(".emoji-menu").removeClass "is-visible"
+ $("#emoji_search").blur()
+ else
+ $(".emoji-menu").addClass "is-visible"
+ $("#emoji_search").focus()
+ else
+ $('.js-add-award').addClass "is-loading"
+ $.get "/emojis", (response) =>
+ $('.js-add-award').removeClass "is-loading"
+ $(".js-award-holder").append response
+ setTimeout =>
+ $(".emoji-menu").addClass "is-visible"
+ $("#emoji_search").focus()
+ @setupSearch()
+ , 200
addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
- $(".emoji-menu").hide()
-
+ $(".emoji-menu").removeClass "is-visible"
+
addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji)
@@ -28,7 +58,7 @@ class @AwardsHandler
if @isActive(emoji)
@decrementCounter(emoji)
else
- counter = @findEmojiIcon(emoji).siblings(".counter")
+ counter = @findEmojiIcon(emoji).siblings(".js-counter")
counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
@@ -42,31 +72,38 @@ class @AwardsHandler
@findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) ->
- counter = @findEmojiIcon(emoji).siblings(".counter")
+ counter = @findEmojiIcon(emoji).siblings(".js-counter")
emojiIcon = counter.parent()
-
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
emojiIcon.removeClass("active")
@removeMeFromAuthorList(emoji)
- else if emoji =="thumbsup" || emoji == "thumbsdown"
+ else if emoji == "thumbsup" || emoji == "thumbsdown"
emojiIcon.tooltip("destroy")
counter.text(0)
emojiIcon.removeClass("active")
+ @removeMeFromAuthorList(emoji)
else
emojiIcon.tooltip("destroy")
emojiIcon.remove()
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
- authors = award_block.attr("data-original-title").split(", ")
- authors = _.without(authors, "me").join(", ")
- award_block.attr("title", authors)
+ authors = award_block
+ .attr("data-original-title")
+ .split(", ")
+ authors.splice(authors.indexOf("me"),1)
+ award_block
+ .closest(".js-emoji-btn")
+ .attr("data-original-title", authors.join(", "))
@resetTooltip(award_block)
addMeToAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
- authors = award_block.attr("data-original-title").split(", ")
+ origTitle = award_block.attr("data-original-title").trim()
+ authors = []
+ if origTitle
+ authors = origTitle.split(', ')
authors.push("me")
award_block.attr("title", authors.join(", "))
@resetTooltip(award_block)
@@ -78,20 +115,24 @@ class @AwardsHandler
setTimeout (->
award.tooltip()
), 200
-
+
createEmoji: (emoji) ->
emojiCssClass = @resolveNameToCssClass(emoji)
nodes = []
- nodes.push("<div class='award active' title='me'>")
- nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>")
- nodes.push("<div class='counter'>1</div>")
- nodes.push("</div>")
-
- emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji)
-
- $(".award").tooltip()
+ nodes.push(
+ "<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>",
+ "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
+ "<span class='award-control-text js-counter'>1</span>",
+ "</button>"
+ )
+
+ emoji_node = $(nodes.join("\n"))
+ .insertBefore(".js-award-holder")
+ .find(".emoji-icon")
+ .data("emoji", emoji)
+ $('.award-control').tooltip()
resolveNameToCssClass: (emoji) ->
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
@@ -114,7 +155,7 @@ class @AwardsHandler
callback.call()
findEmojiIcon: (emoji) ->
- $(".award [data-emoji='#{emoji}']")
+ $(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: ->
$('body, html').animate({
@@ -150,13 +191,13 @@ class @AwardsHandler
term = $(ev.target).val()
# Clean previous search results
- $("ul.emoji-search,h5.emoji-search").remove()
+ $("ul.emoji-menu-search, h5.emoji-search").remove()
if term
# Generate a search result block
h5 = $("<h5>").text("Search results").addClass("emoji-search")
found_emojis = @searchEmojis(term).show()
- ul = $("<ul>").addClass("emoji-search").append(found_emojis)
+ ul = $("<ul>").addClass("emoji-menu-list emoji-menu-search").append(found_emojis)
$(".emoji-menu-content ul, .emoji-menu-content h5").hide()
$(".emoji-menu-content").append(h5).append(ul)
else
diff --git a/app/assets/javascripts/behaviors/autosize.js.coffee b/app/assets/javascripts/behaviors/autosize.js.coffee
new file mode 100644
index 00000000000..a072fe48a98
--- /dev/null
+++ b/app/assets/javascripts/behaviors/autosize.js.coffee
@@ -0,0 +1,22 @@
+#= require jquery.ba-resize
+#= require autosize
+
+$ ->
+ $fields = $('.js-autosize')
+
+ $fields.on 'autosize:resized', ->
+ $field = $(@)
+ $field.data('height', $field.outerHeight())
+
+ $fields.on 'resize.autosize', ->
+ $field = $(@)
+
+ if $field.data('height') != $field.outerHeight()
+ $field.data('height', $field.outerHeight())
+ autosize.destroy($field)
+ $field.css('max-height', window.outerHeight)
+
+ autosize($fields)
+ autosize.update($fields)
+
+ $fields.css('resize', 'vertical')
diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee
index 4ec8531d580..6e29d374267 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js.coffee
+++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee
@@ -1,29 +1,52 @@
# Quick Submit behavior
#
-# When an input field with the `js-quick-submit` class receives a "Meta+Enter"
-# (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, its parent form is
-# submitted.
+# When a child field of a form with a `js-quick-submit` class receives a
+# "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
+# is submitted.
#
#= require extensions/jquery
#
# ### Example Markup
#
-# <form action="/foo">
-# <input type="text" class="js-quick-submit" />
-# <textarea class="js-quick-submit"></textarea>
+# <form action="/foo" class="js-quick-submit">
+# <input type="text" />
+# <textarea></textarea>
+# <input type="submit" value="Submit" />
# </form>
#
+isMac = ->
+ navigator.userAgent.match(/Macintosh/)
+
+keyCodeIs = (e, keyCode) ->
+ return false if (e.originalEvent && e.originalEvent.repeat) || e.repeat
+ return e.keyCode == keyCode
+
$(document).on 'keydown.quick_submit', '.js-quick-submit', (e) ->
- return if (e.originalEvent && e.originalEvent.repeat) || e.repeat
- return unless e.keyCode == 13 # Enter
+ return unless keyCodeIs(e, 13) # Enter
- if navigator.userAgent.match(/Macintosh/)
- return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey)
- else
- return unless (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey)
+ return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey)
e.preventDefault()
$form = $(e.target).closest('form')
$form.find('input[type=submit], button[type=submit]').disable()
$form.submit()
+
+# If the user tabs to a submit button on a `js-quick-submit` form, display a
+# tooltip to let them know they could've used the hotkey
+$(document).on 'keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', (e) ->
+ return unless keyCodeIs(e, 9) # Tab
+
+ if isMac()
+ title = "You can also press &#8984;-Enter"
+ else
+ title = "You can also press Ctrl-Enter"
+
+ $this = $(@)
+ $this.tooltip(
+ container: 'body'
+ html: 'true'
+ placement: 'auto top'
+ title: title
+ trigger: 'manual'
+ ).tooltip('show').one('blur', -> $this.tooltip('hide'))
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
index f6bf836f19f..390e41ed8d4 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -32,6 +32,7 @@ class @EditBlob
content: editor.getValue()
, (response) ->
currentPane.empty().append response
+ currentPane.syntaxHighlight()
return
else
diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/branch-graph.js.coffee
index 917228bd276..f2fd2a775a4 100644
--- a/app/assets/javascripts/branch-graph.js.coffee
+++ b/app/assets/javascripts/branch-graph.js.coffee
@@ -66,7 +66,7 @@ class @BranchGraph
r.rect(40, 0, 30, @barHeight).attr fill: "#444"
for day, mm in @days
- if cuday isnt day[0]
+ if cuday isnt day[0] || cumonth isnt day[1]
# Dates
r.text(55, @offsetY + @unitTime * mm, day[0])
.attr(
diff --git a/app/assets/javascripts/breakpoints.coffee b/app/assets/javascripts/breakpoints.coffee
new file mode 100644
index 00000000000..5457430f921
--- /dev/null
+++ b/app/assets/javascripts/breakpoints.coffee
@@ -0,0 +1,37 @@
+class @Breakpoints
+ instance = null;
+
+ class BreakpointInstance
+ BREAKPOINTS = ["xs", "sm", "md", "lg"]
+
+ constructor: ->
+ @setup()
+
+ setup: ->
+ allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
+ ".device-#{breakpoint}"
+ return if $(allDeviceSelector.join(",")).length
+
+ # Create all the elements
+ els = $.map BREAKPOINTS, (breakpoint) ->
+ "<div class='device-#{breakpoint} visible-#{breakpoint}'></div>"
+ $("body").append els.join('')
+
+ visibleDevice: ->
+ allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
+ ".device-#{breakpoint}"
+ $(allDeviceSelector.join(",")).filter(":visible")
+
+ getBreakpointSize: ->
+ $visibleDevice = @visibleDevice
+ # the page refreshed via turbolinks
+ if not $visibleDevice().length
+ @setup()
+ $visibleDevice = @visibleDevice()
+ return $visibleDevice.attr("class").split("visible-")[1]
+
+ @get: ->
+ return instance ?= new BreakpointInstance
+
+$ =>
+ @bp = Breakpoints.get()
diff --git a/app/assets/javascripts/broadcast_message.js.coffee b/app/assets/javascripts/broadcast_message.js.coffee
new file mode 100644
index 00000000000..a38a329c4c2
--- /dev/null
+++ b/app/assets/javascripts/broadcast_message.js.coffee
@@ -0,0 +1,22 @@
+$ ->
+ $('input#broadcast_message_color').on 'input', ->
+ previewColor = $(@).val()
+ $('div.broadcast-message-preview').css('background-color', previewColor)
+
+ $('input#broadcast_message_font').on 'input', ->
+ previewColor = $(@).val()
+ $('div.broadcast-message-preview').css('color', previewColor)
+
+ previewPath = $('textarea#broadcast_message_message').data('preview-path')
+
+ $('textarea#broadcast_message_message').on 'input', ->
+ message = $(@).val()
+
+ if message == ''
+ $('.js-broadcast-message-preview').text("Your message here")
+ else
+ $.ajax(
+ url: previewPath
+ type: "POST"
+ data: { broadcast_message: { message: message } }
+ )
diff --git a/app/assets/javascripts/build_artifacts.js.coffee b/app/assets/javascripts/build_artifacts.js.coffee
new file mode 100644
index 00000000000..5ae6cba56c8
--- /dev/null
+++ b/app/assets/javascripts/build_artifacts.js.coffee
@@ -0,0 +1,14 @@
+class @BuildArtifacts
+ constructor: () ->
+ @disablePropagation()
+ @setupEntryClick()
+
+ disablePropagation: ->
+ $('.top-block').on 'click', '.download', (e) ->
+ e.stopPropagation()
+ $('.tree-holder').on 'click', 'tr[data-link] a', (e) ->
+ e.stopImmediatePropagation()
+
+ setupEntryClick: ->
+ $('.tree-holder').on 'click', 'tr[data-link]', (e) ->
+ window.location = @dataset.link
diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee
index 97621236924..d80e0e716ce 100644
--- a/app/assets/javascripts/calendar.js.coffee
+++ b/app/assets/javascripts/calendar.js.coffee
@@ -1,9 +1,4 @@
class @Calendar
- options =
- month: "short"
- day: "numeric"
- year: "numeric"
-
constructor: (timestamps, starting_year, starting_month, calendar_activities_path) ->
cal = new CalHeatMap()
cal.init
diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
index 44d5ddb7d95..7afe8bf79e2 100644
--- a/app/assets/javascripts/ci/build.coffee
+++ b/app/assets/javascripts/ci/build.coffee
@@ -4,6 +4,8 @@ class CiBuild
constructor: (build_url, build_status) ->
clearInterval(CiBuild.interval)
+ @initScrollButtonAffix()
+
if build_status == "running" || build_status == "pending"
#
# Bind autoscroll button to follow build output
@@ -38,4 +40,15 @@ class CiBuild
checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
+ initScrollButtonAffix: ->
+ $buildScroll = $('#js-build-scroll')
+ $body = $('body')
+ $buildTrace = $('#build-trace')
+
+ $buildScroll.affix(
+ offset:
+ bottom: ->
+ $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
+ )
+
@CiBuild = CiBuild
diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee
index c183e78e513..ffd3627b1b0 100644
--- a/app/assets/javascripts/commits.js.coffee
+++ b/app/assets/javascripts/commits.js.coffee
@@ -1,15 +1,5 @@
class @CommitsList
- @data =
- ref: null
- limit: 0
- offset: 0
- @disable = false
-
- @showProgress: ->
- $('.loading').show()
-
- @hideProgress: ->
- $('.loading').hide()
+ @timer = null
@init: (ref, limit) ->
$("body").on "click", ".day-commits-table li.commit", (event) ->
@@ -18,38 +8,32 @@ class @CommitsList
e.stopPropagation()
return false
- @data.ref = ref
- @data.limit = limit
- @data.offset = limit
+ Pager.init limit, false
+
+ @content = $("#commits-list")
+ @searchField = $("#commits-search")
+ @initSearch()
- this.initLoadMore()
- this.showProgress()
+ @initSearch: ->
+ @timer = null
+ @searchField.keyup =>
+ clearTimeout(@timer)
+ @timer = setTimeout(@filterResults, 500)
+
+ @filterResults: =>
+ form = $(".commits-search-form")
+ search = @searchField.val()
+ commitsUrl = form.attr("action") + '?' + form.serialize()
+ @content.fadeTo('fast', 0.5)
- @getOld: ->
- this.showProgress()
$.ajax
type: "GET"
- url: location.href
- data: @data
- complete: this.hideProgress
- success: (data) ->
- CommitsList.append(data.count, data.html)
+ url: form.attr("action")
+ data: form.serialize()
+ complete: =>
+ @content.fadeTo('fast', 1.0)
+ success: (data) =>
+ @content.html(data.html)
+ # Change url so if user reload a page - search results are saved
+ history.replaceState {page: commitsUrl}, document.title, commitsUrl
dataType: "json"
-
- @append: (count, html) ->
- $("#commits-list").append(html)
- if count > 0
- @data.offset += count
- else
- @disable = true
-
- @initLoadMore: ->
- $(document).unbind('scroll')
- $(document).endlessScroll
- bottomPixels: 400
- fireDelay: 1000
- fireOnce: true
- ceaseFire: =>
- @disable
- callback: =>
- this.getOld()
diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee
deleted file mode 100644
index 00ee503ff16..00000000000
--- a/app/assets/javascripts/dashboard.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-class @Dashboard
- constructor: ->
- new ProjectsList()
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 69e061ce6e9..f5e1ca9860d 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -14,7 +14,6 @@ class Dispatcher
path = page.split(':')
shortcut_handler = null
-
switch page
when 'projects:issues:index'
Issues.init()
@@ -23,8 +22,10 @@ class Dispatcher
new Issue()
shortcut_handler = new ShortcutsIssuable()
new ZenMode()
- when 'projects:milestones:show'
+ when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone()
+ when 'dashboard:todos:index'
+ new Todos()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
new DropzoneInput($('.milestone-form'))
@@ -57,8 +58,6 @@ class Dispatcher
when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation()
MergeRequests.init()
- when 'dashboard:show', 'root:show'
- new Dashboard()
when 'dashboard:activity'
new Activities()
when 'dashboard:projects:starred'
@@ -74,8 +73,11 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
when 'projects:show'
shortcut_handler = new ShortcutsNavigation()
- when 'groups:show'
+
+ new TreeView() if $('#tree-slider').length
+ when 'groups:activity'
new Activities()
+ when 'groups:show'
shortcut_handler = new ShortcutsNavigation()
when 'groups:group_members:index'
new GroupMembers()
@@ -86,9 +88,11 @@ class Dispatcher
when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new'
new GroupAvatar()
when 'projects:tree:show'
- new TreeView()
shortcut_handler = new ShortcutsNavigation()
- when 'projects:blob:show'
+ new TreeView()
+ when 'projects:find_file:show'
+ shortcut_handler = true
+ when 'projects:blob:show', 'projects:blame:show'
new LineHighlighter()
shortcut_handler = new ShortcutsNavigation()
when 'projects:labels:new', 'projects:labels:edit'
@@ -99,9 +103,10 @@ class Dispatcher
shortcut_handler = true
when 'projects:forks:new'
new ProjectFork()
- when 'users:show'
- new User()
- new Activities()
+ when 'projects:artifacts:browse'
+ new BuildArtifacts()
+ when 'projects:group_links:index'
+ new GroupsSelect()
switch path.first()
when 'admin'
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index 30a35a04339..b502131a99d 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -65,8 +65,7 @@ class @DropzoneInput
return
success: (header, response) ->
- child = $(dropzone[0]).children("textarea")
- $(child).val $(child).val() + formatLink(response.link) + "\n"
+ pasteText response.link.markdown
return
error: (temp, errorMessage) ->
@@ -99,11 +98,6 @@ class @DropzoneInput
child = $(dropzone[0]).children("textarea")
- formatLink = (link) ->
- text = "[#{link.alt}](#{link.url})"
- text = "!#{text}" if link.is_image
- text
-
handlePaste = (event) ->
pasteEvent = event.originalEvent
if pasteEvent.clipboardData and pasteEvent.clipboardData.items
@@ -133,6 +127,7 @@ class @DropzoneInput
beforeSelection = $(child).val().substring 0, caretStart
afterSelection = $(child).val().substring caretEnd, textEnd
$(child).val beforeSelection + text + afterSelection
+ child.get(0).setSelectionRange caretStart + text.length, caretEnd + text.length
form_textarea.trigger "input"
getFilename = (e) ->
@@ -162,7 +157,7 @@ class @DropzoneInput
closeAlertMessage()
success: (e, textStatus, response) ->
- insertToTextArea(filename, formatLink(response.responseJSON.link))
+ insertToTextArea(filename, response.responseJSON.link.markdown)
error: (response) ->
showError(response.responseJSON.message)
@@ -202,8 +197,3 @@ class @DropzoneInput
e.preventDefault()
$(@).closest('.gfm-form').find('.div-dropzone').click()
return
-
- formatLink: (link) ->
- text = "[#{link.alt}](#{link.url})"
- text = "!#{text}" if link.is_image
- text
diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee
index 7967892f856..4718bcf7a1e 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.coffee
+++ b/app/assets/javascripts/gfm_auto_complete.js.coffee
@@ -34,7 +34,7 @@ GitLab.GfmAutoComplete =
searchKey: 'search'
callbacks:
beforeSave: (members) ->
- $.map members, (m) ->
+ $.map members, (m) ->
title = m.name
title += " (#{m.count})" if m.count
@@ -50,7 +50,7 @@ GitLab.GfmAutoComplete =
insertTpl: '${atwho-at}${id}'
callbacks:
beforeSave: (issues) ->
- $.map issues, (i) ->
+ $.map issues, (i) ->
id: i.iid
title: sanitize(i.title)
search: "#{i.iid} #{i.title}"
@@ -63,12 +63,12 @@ GitLab.GfmAutoComplete =
insertTpl: '${atwho-at}${id}'
callbacks:
beforeSave: (merges) ->
- $.map merges, (m) ->
+ $.map merges, (m) ->
id: m.iid
title: sanitize(m.title)
search: "#{m.iid} #{m.title}"
- input.one 'focus', =>
+ if @dataSource
$.getJSON(@dataSource).done (data) ->
# load members
input.atwho 'load', '@', data.members
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
new file mode 100644
index 00000000000..c81e8bf760a
--- /dev/null
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -0,0 +1,276 @@
+class GitLabDropdownFilter
+ BLUR_KEYCODES = [27, 40]
+
+ constructor: (@dropdown, @options) ->
+ @input = @dropdown.find(".dropdown-input .dropdown-input-field")
+
+ # Key events
+ timeout = ""
+ @input.on "keyup", (e) =>
+ if e.keyCode is 13 && @input.val() isnt ""
+ if @options.enterCallback
+ @options.enterCallback()
+ return
+
+ clearTimeout timeout
+ timeout = setTimeout =>
+ blur_field = @shouldBlur e.keyCode
+ search_text = @input.val()
+
+ if blur_field
+ @input.blur()
+
+ if @options.remote
+ @options.query search_text, (data) =>
+ @options.callback(data)
+ else
+ @filter search_text
+ , 250
+
+ shouldBlur: (keyCode) ->
+ return BLUR_KEYCODES.indexOf(keyCode) >= 0
+
+ filter: (search_text) ->
+ data = @options.data()
+ results = data
+
+ if search_text isnt ""
+ results = fuzzaldrinPlus.filter(data, search_text,
+ key: @options.keys
+ )
+
+ @options.callback results
+
+class GitLabDropdownRemote
+ constructor: (@dataEndpoint, @options) ->
+
+ execute: ->
+ if typeof @dataEndpoint is "string"
+ @fetchData()
+ else if typeof @dataEndpoint is "function"
+ if @options.beforeSend
+ @options.beforeSend()
+
+ # Fetch the data by calling the data funcfion
+ @dataEndpoint "", (data) =>
+ if @options.success
+ @options.success(data)
+
+ if @options.beforeSend
+ @options.beforeSend()
+
+ # Fetch the data through ajax if the data is a string
+ fetchData: ->
+ $.ajax(
+ url: @dataEndpoint,
+ dataType: @options.dataType,
+ beforeSend: =>
+ if @options.beforeSend
+ @options.beforeSend()
+ success: (data) =>
+ if @options.success
+ @options.success(data)
+ )
+
+class GitLabDropdown
+ LOADING_CLASS = "is-loading"
+ PAGE_TWO_CLASS = "is-page-two"
+ ACTIVE_CLASS = "is-active"
+
+ constructor: (@el, @options) ->
+ self = @
+ @dropdown = $(@el).parent()
+ search_fields = if @options.search then @options.search.fields else [];
+
+ if @options.data
+ # Remote data
+ @remote = new GitLabDropdownRemote @options.data, {
+ dataType: @options.dataType,
+ beforeSend: @toggleLoading.bind(@)
+ success: (data) =>
+ @fullData = data
+
+ @parseData @fullData
+ }
+
+ # Init filiterable
+ if @options.filterable
+ @filter = new GitLabDropdownFilter @dropdown,
+ remote: @options.filterRemote
+ query: @options.data
+ keys: @options.search.fields
+ data: =>
+ return @fullData
+ callback: (data) =>
+ @parseData data
+ enterCallback: =>
+ @selectFirstRow()
+
+ # Event listeners
+ @dropdown.on "shown.bs.dropdown", @opened
+ @dropdown.on "hidden.bs.dropdown", @hidden
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
+ e.preventDefault()
+ e.stopPropagation()
+
+ @togglePage()
+
+ if @options.selectable
+ selector = ".dropdown-content a"
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content a"
+
+ @dropdown.on "click", selector, (e) ->
+ self.rowClicked $(@)
+
+ if self.options.clicked
+ self.options.clicked()
+
+ toggleLoading: ->
+ $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
+
+ togglePage: ->
+ menu = $('.dropdown-menu', @dropdown)
+
+ if menu.hasClass(PAGE_TWO_CLASS)
+ if @remote
+ @remote.execute()
+
+ menu.toggleClass PAGE_TWO_CLASS
+
+ parseData: (data) ->
+ @renderedData = data
+
+ # Render each row
+ html = $.map data, (obj) =>
+ return @renderItem(obj)
+
+ if @options.filterable and data.length is 0
+ # render no matching results
+ html = [@noResults()]
+
+ # Render the full menu
+ full_html = @renderMenu(html.join(""))
+
+ @appendMenu(full_html)
+
+ opened: =>
+ contentHtml = $('.dropdown-content', @dropdown).html()
+ if @remote && contentHtml is ""
+ @remote.execute()
+
+ if @options.filterable
+ @dropdown.find(".dropdown-input-field").focus()
+
+ hidden: =>
+ if @options.filterable
+ @dropdown.find(".dropdown-input-field").blur().val("")
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
+
+
+ # Render the full menu
+ renderMenu: (html) ->
+ menu_html = ""
+
+ if @options.renderMenu
+ menu_html = @options.renderMenu(html)
+ else
+ menu_html = "<ul>#{html}</ul>"
+
+ return menu_html
+
+ # Append the menu into the dropdown
+ appendMenu: (html) ->
+ selector = '.dropdown-content'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content"
+
+ $(selector, @dropdown).html html
+
+ # Render the row
+ renderItem: (data) ->
+ html = ""
+
+ return "<li class='divider'></li>" if data is "divider"
+
+ if @options.renderRow
+ # Call the render function
+ html = @options.renderRow(data)
+ else
+ selected = if @options.isSelected then @options.isSelected(data) else false
+ url = if @options.url then @options.url(data) else "#"
+ text = if @options.text then @options.text(data) else ""
+ cssClass = "";
+
+ if selected
+ cssClass = "is-active"
+
+ html = "<li>"
+ html += "<a href='#{url}' class='#{cssClass}'>"
+ html += text
+ html += "</a>"
+ html += "</li>"
+
+ return html
+
+ noResults: ->
+ html = "<li>"
+ html += "<a href='#' class='is-focused'>"
+ html += "No matching results."
+ html += "</a>"
+ html += "</li>"
+
+ rowClicked: (el) ->
+ fieldName = @options.fieldName
+ field = @dropdown.parent().find("input[name='#{fieldName}']")
+
+ if el.hasClass(ACTIVE_CLASS)
+ field.remove()
+ else
+ fieldName = @options.fieldName
+ selectedIndex = el.parent().index()
+ if @renderedData
+ selectedObject = @renderedData[selectedIndex]
+ value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
+
+ if !value?
+ field.remove()
+
+ if @options.multiSelect
+ oldValue = field.val()
+ if oldValue
+ value = "#{oldValue},#{value}"
+ else
+ @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
+
+ # Toggle active class for the tick mark
+ el.toggleClass "is-active"
+
+ # Toggle the dropdown label
+ if @options.toggleLabel
+ $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
+
+ if value?
+ if !field.length
+ # Create hidden input for form
+ input = "<input type='hidden' name='#{fieldName}' />"
+ @dropdown.before input
+
+ @dropdown.parent().find("input[name='#{fieldName}']").val value
+
+ selectFirstRow: ->
+ selector = '.dropdown-content li:first-child a'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content li:first-child a"
+
+ # similute a click on the first link
+ $(selector).trigger "click"
+
+$.fn.glDropdown = (opts) ->
+ return @.each ->
+ new GitLabDropdown @, opts
diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee
index 02232698bc2..e52b73f94f6 100644
--- a/app/assets/javascripts/issuable_context.js.coffee
+++ b/app/assets/javascripts/issuable_context.js.coffee
@@ -10,20 +10,10 @@ class @IssuableContext
$(".issuable-sidebar .inline-update").on "change", ".js-assignee", ->
$(this).submit()
- $('.issuable-details').waitForImages ->
- $('.issuable-affix').on 'affix.bs.affix', ->
- $(@).width($(@).outerWidth())
- .on 'affixed-top.bs.affix affixed-bottom.bs.affix', ->
- $(@).width('')
-
- $('.issuable-affix').affix offset:
- top: ->
- @top = ($('.issuable-affix').offset().top - 70)
- bottom: ->
- @bottom = $('.footer').outerHeight(true)
-
- $(".edit-link").click (e) ->
+ $(document).on "click",".edit-link", (e) ->
block = $(@).parents('.block')
block.find('.selectbox').show()
block.find('.value').hide()
block.find('.js-select2').select2("open")
+
+ $(".right-sidebar").niceScroll()
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index c256ec8f41b..d663e34871c 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -6,22 +6,40 @@ class @Issue
constructor: ->
# Prevent duplicate event bindings
@disableTaskList()
-
+ @fixAffixScroll()
if $('a.btn-close').length
@initTaskList()
@initIssueBtnEventListeners()
+ fixAffixScroll: ->
+ fixAffix = ->
+ $discussion = $('.issuable-discussion')
+ $sidebar = $('.issuable-sidebar')
+ if $sidebar.hasClass('no-affix')
+ $sidebar.removeClass(['affix-top','affix'])
+ discussionHeight = $discussion.height()
+ sidebarHeight = $sidebar.height()
+ if sidebarHeight > discussionHeight
+ $discussion.height(sidebarHeight + 50)
+ $sidebar.addClass('no-affix')
+ $(window).on('resize', fixAffix)
+ fixAffix()
+
initTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
initIssueBtnEventListeners: ->
+ _this = @
issueFailMessage = 'Unable to update this issue at this time.'
$('a.btn-close, a.btn-reopen').on 'click', (e) ->
e.preventDefault()
e.stopImmediatePropagation()
$this = $(this)
isClose = $this.hasClass('btn-close')
+ shouldSubmit = $this.hasClass('btn-comment')
+ if shouldSubmit
+ _this.submitNoteForm($this.closest('form'))
$this.prop('disabled', true)
url = $this.attr('href')
$.ajax
@@ -32,12 +50,14 @@ class @Issue
new Flash(issueFailMessage, 'alert')
success: (data, textStatus, jqXHR) ->
if data.saved
- $this.addClass('hidden')
+ $(document).trigger('issuable:change');
if isClose
+ $('a.btn-close').addClass('hidden')
$('a.btn-reopen').removeClass('hidden')
$('div.status-box-closed').removeClass('hidden')
$('div.status-box-open').addClass('hidden')
else
+ $('a.btn-reopen').addClass('hidden')
$('a.btn-close').removeClass('hidden')
$('div.status-box-closed').addClass('hidden')
$('div.status-box-open').removeClass('hidden')
@@ -45,6 +65,11 @@ class @Issue
new Flash(issueFailMessage, 'alert')
$this.prop('disabled', false)
+ submitNoteForm: (form) =>
+ noteText = form.find("textarea.js-note-text").val()
+ if noteText.trim().length > 0
+ form.submit()
+
disableTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
diff --git a/app/assets/javascripts/issue_status_select.js.coffee b/app/assets/javascripts/issue_status_select.js.coffee
new file mode 100644
index 00000000000..c5740f27ddd
--- /dev/null
+++ b/app/assets/javascripts/issue_status_select.js.coffee
@@ -0,0 +1,11 @@
+class @IssueStatusSelect
+ constructor: ->
+ $('.js-issue-status').each (i, el) ->
+ fieldName = $(el).data("field-name")
+
+ $(el).glDropdown(
+ selectable: true
+ fieldName: fieldName
+ id: (obj, el) ->
+ $(el).data("id")
+ )
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index ac9e022e727..1127b289264 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -15,13 +15,6 @@
$(this).html totalIssues + 1
else
$(this).html totalIssues - 1
- $("body").on "click", ".issues-other-filters .dropdown-menu a", ->
- $('.issues-list').block(
- message: null,
- overlayCSS:
- backgroundColor: '#DDD'
- opacity: .4
- )
reload: ->
Issues.initSelects()
@@ -48,24 +41,28 @@
@timer = null
$("#issue_search").keyup ->
clearTimeout(@timer)
- @timer = setTimeout(Issues.filterResults, 500)
+ @timer = setTimeout( ->
+ Issues.filterResults $("#issue_search_form")
+ , 500)
- filterResults: =>
- form = $("#issue_search_form")
- search = $("#issue_search").val()
- $('.issues-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '? '+ form.serialize()
+ filterResults: (form) =>
+ $('.issues-holder, .merge-requests-holder').css("opacity", '0.5')
+ formAction = form.attr('action')
+ formData = form.serialize()
+ issuesUrl = formAction
+ issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}")
+ issuesUrl += formData
$.ajax
type: "GET"
- url: form.attr('action')
- data: form.serialize()
+ url: formAction
+ data: formData
complete: ->
- $('.issues-holder').css("opacity", '1.0')
+ $('.issues-holder, .merge-requests-holder').css("opacity", '1.0')
success: (data) ->
- $('.issues-holder').html(data.html)
+ $('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
- History.replaceState {page: issues_url}, document.title, issues_url
+ history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload()
dataType: "json"
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
new file mode 100644
index 00000000000..e6c1446f14f
--- /dev/null
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -0,0 +1,109 @@
+class @LabelsSelect
+ constructor: ->
+ $('.js-label-select').each (i, dropdown) ->
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ labelUrl = $dropdown.data('labels')
+ selectedLabel = $dropdown.data('selected')
+ if selectedLabel
+ selectedLabel = selectedLabel.split(',')
+ newLabelField = $('#new_label_name')
+ newColorField = $('#new_label_color')
+ showNo = $dropdown.data('show-no')
+ showAny = $dropdown.data('show-any')
+ defaultLabel = $dropdown.text().trim()
+
+ if newLabelField.length
+ $('.suggest-colors-dropdown a').on 'click', (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+ newColorField.val $(this).data('color')
+ $('.js-dropdown-label-color-preview')
+ .css 'background-color', $(this).data('color')
+ .addClass 'is-active'
+
+ $('.js-new-label-btn').on 'click', (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+
+ if newLabelField.val() isnt '' and newColorField.val() isnt ''
+ $('.js-new-label-btn').disable()
+
+ # Create new label with API
+ Api.newLabel projectId, {
+ name: newLabelField.val()
+ color: newColorField.val()
+ }, (label) ->
+ $('.js-new-label-btn').enable()
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
+
+ $dropdown.glDropdown(
+ data: (term, callback) ->
+ # We have to fetch the JS version of the labels list because there is no
+ # public facing JSON url for labels
+ $.ajax(
+ url: labelUrl
+ ).done (data) ->
+ html = $(data)
+ data = []
+ html.find('.label-row a').each ->
+ data.push(
+ title: $(@).text().trim()
+ )
+
+ if showNo
+ data.unshift(
+ id: 0
+ title: 'No Label'
+ )
+
+ if showAny
+ data.unshift(
+ isAny: true
+ title: 'Any Label'
+ )
+
+ if data.length > 2
+ data.splice 2, 0, 'divider'
+
+ callback data
+ renderRow: (label) ->
+ if $.isArray(selectedLabel)
+ selected = ''
+ $.each selectedLabel, (i, selectedLbl) ->
+ selectedLbl = selectedLbl.trim()
+ if selected is '' and label.title is selectedLbl
+ selected = 'is-active'
+ else
+ selected = if label.title is selectedLabel then 'is-active' else ''
+
+ "<li>
+ <a href='#' class='#{selected}'>
+ #{label.title}
+ </a>
+ </li>"
+ filterable: true
+ search:
+ fields: ['title']
+ selectable: true
+ toggleLabel: (selected) ->
+ if selected and selected.title isnt 'Any Label'
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
+ id: (label) ->
+ if label.isAny?
+ ''
+ else
+ label.title
+ clicked: ->
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ )
diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee
new file mode 100644
index 00000000000..d14b7139237
--- /dev/null
+++ b/app/assets/javascripts/logo.js.coffee
@@ -0,0 +1,50 @@
+Turbolinks.enableProgressBar();
+
+defaultClass = 'tanuki-shape'
+pieces = [
+ 'path#tanuki-right-cheek',
+ 'path#tanuki-right-eye, path#tanuki-right-ear',
+ 'path#tanuki-nose',
+ 'path#tanuki-left-eye, path#tanuki-left-ear',
+ 'path#tanuki-left-cheek',
+]
+pieceIndex = 0
+firstPiece = pieces[0]
+
+currentTimer = null
+delay = 150
+
+clearHighlights = ->
+ $(".#{defaultClass}.highlight").attr('class', defaultClass)
+
+start = ->
+ clearHighlights()
+ pieceIndex = 0
+ pieces.reverse() unless pieces[0] == firstPiece
+ clearInterval(currentTimer) if currentTimer
+ currentTimer = setInterval(work, delay)
+
+stop = ->
+ clearInterval(currentTimer)
+ clearHighlights()
+
+work = ->
+ clearHighlights()
+ $(pieces[pieceIndex]).attr('class', "#{defaultClass} highlight")
+
+ # If we hit the last piece, reset the index and then reverse the array to
+ # get a nice back-and-forth sweeping look
+ if pieceIndex == pieces.length - 1
+ pieceIndex = 0
+ pieces.reverse()
+ else
+ pieceIndex++
+
+$(document).on('page:fetch', start)
+$(document).on('page:change', stop)
+
+$ ->
+ # Make logo clickable as part of a workaround for Safari visited
+ # link behaviour (See !2690).
+ $('#logo').on 'click', ->
+ $('#js-shortcuts-home').get(0).click()
diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee
index 98fc8f17340..2a0b9479445 100644
--- a/app/assets/javascripts/markdown_preview.js.coffee
+++ b/app/assets/javascripts/markdown_preview.js.coffee
@@ -6,6 +6,7 @@
class @MarkdownPreview
# Minimum number of users referenced before triggering a warning
referenceThreshold: 10
+ ajaxCache: {}
showPreview: (form) ->
preview = form.find('.js-md-preview')
@@ -24,12 +25,16 @@ class @MarkdownPreview
renderMarkdown: (text, success) ->
return unless window.markdown_preview_path
+ return success(@ajaxCache.response) if text == @ajaxCache.text
+
$.ajax
type: 'POST'
url: window.markdown_preview_path
data: { text: text }
dataType: 'json'
- success: success
+ success: (response) =>
+ @ajaxCache = text: text, response: response
+ success(response)
hideReferencedUsers: (form) ->
referencedUsers = form.find('.referenced-users')
@@ -49,6 +54,7 @@ markdownPreview = new MarkdownPreview()
previewButtonSelector = '.js-md-preview-button'
writeButtonSelector = '.js-md-write-button'
+lastTextareaPreviewed = null
$.fn.setupMarkdownPreview = ->
$form = $(this)
@@ -58,10 +64,10 @@ $.fn.setupMarkdownPreview = ->
form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form)
form_textarea.on 'blur', -> markdownPreview.showPreview($form)
-$(document).on 'click', previewButtonSelector, (e) ->
- e.preventDefault()
+$(document).on 'markdown-preview:show', (e, $form) ->
+ return unless $form
- $form = $(this).closest('form')
+ lastTextareaPreviewed = $form.find('textarea.markdown-area')
# toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active')
@@ -73,10 +79,10 @@ $(document).on 'click', previewButtonSelector, (e) ->
markdownPreview.showPreview($form)
-$(document).on 'click', writeButtonSelector, (e) ->
- e.preventDefault()
+$(document).on 'markdown-preview:hide', (e, $form) ->
+ return unless $form
- $form = $(this).closest('form')
+ lastTextareaPreviewed = null
# toggle tabs
$form.find(writeButtonSelector).parent().addClass('active')
@@ -84,4 +90,30 @@ $(document).on 'click', writeButtonSelector, (e) ->
# toggle content
$form.find('.md-write-holder').show()
+ $form.find('textarea.markdown-area').focus()
$form.find('.md-preview-holder').hide()
+
+$(document).on 'markdown-preview:toggle', (e, keyboardEvent) ->
+ $target = $(keyboardEvent.target)
+
+ if $target.is('textarea.markdown-area')
+ $(document).triggerHandler('markdown-preview:show', [$target.closest('form')])
+ keyboardEvent.preventDefault()
+ else if lastTextareaPreviewed
+ $target = lastTextareaPreviewed
+ $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')])
+ keyboardEvent.preventDefault()
+
+$(document).on 'click', previewButtonSelector, (e) ->
+ e.preventDefault()
+
+ $form = $(this).closest('form')
+
+ $(document).triggerHandler('markdown-preview:show', [$form])
+
+$(document).on 'click', writeButtonSelector, (e) ->
+ e.preventDefault()
+
+ $form = $(this).closest('form')
+
+ $(document).triggerHandler('markdown-preview:hide', [$form])
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index 9047587db81..6af5a48a0bb 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -15,10 +15,13 @@ class @MergeRequest
this.$('.show-all-commits').on 'click', =>
this.showAllCommits()
+ @fixAffixScroll();
+
@initTabs()
# Prevent duplicate event bindings
@disableTaskList()
+ @initMRBtnListeners()
if $("a.btn-close").length
@initTaskList()
@@ -27,6 +30,20 @@ class @MergeRequest
$: (selector) ->
this.$el.find(selector)
+ fixAffixScroll: ->
+ fixAffix = ->
+ $discussion = $('.issuable-discussion')
+ $sidebar = $('.issuable-sidebar')
+ if $sidebar.hasClass('no-affix')
+ $sidebar.removeClass(['affix-top','affix'])
+ discussionHeight = $discussion.height()
+ sidebarHeight = $sidebar.height()
+ if sidebarHeight > discussionHeight
+ $discussion.height(sidebarHeight + 50)
+ $sidebar.addClass('no-affix')
+ $(window).on('resize', fixAffix)
+ fixAffix()
+
initTabs: ->
if @opts.action != 'new'
# `MergeRequests#new` has no tab-persisting or lazy-loading behavior
@@ -43,6 +60,28 @@ class @MergeRequest
$('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
+ initMRBtnListeners: ->
+ _this = @
+ $('a.btn-close, a.btn-reopen').on 'click', (e) ->
+ $this = $(this)
+ shouldSubmit = $this.hasClass('btn-comment')
+ if shouldSubmit && $this.data('submitted')
+ return
+ if shouldSubmit
+ if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ _this.submitNoteForm($this.closest('form'),$this)
+
+
+ submitNoteForm: (form, $button) =>
+ noteText = form.find("textarea.js-note-text").val()
+ if noteText.trim().length > 0
+ form.submit()
+ $button.data('submitted',true)
+ $button.trigger('click')
+
+
disableTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 9e2dc1250c9..8322b4c46ad 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -5,7 +5,7 @@
#
# ### Example Markup
#
-# <ul class="nav nav-tabs merge-request-tabs">
+# <ul class="nav-links merge-request-tabs">
# <li class="notes-tab active">
# <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
# Discussion
@@ -70,6 +70,7 @@ class @MergeRequestTabs
@loadCommits($target.attr('href'))
else if action == 'diffs'
@loadDiff($target.attr('href'))
+ @shrinkView()
else if action == 'builds'
@loadBuilds($target.attr('href'))
@@ -145,7 +146,9 @@ class @MergeRequestTabs
url: "#{source}.json" + @_location.search
success: (data) =>
document.querySelector("div#diffs").innerHTML = data.html
+ $('.js-timeago').timeago()
$('div#diffs .js-syntax-highlight').syntaxHighlight()
+ @expandViewContainer() if @diffViewType() is 'parallel'
@diffsLoaded = true
@scrollToElement("#diffs")
@@ -177,3 +180,20 @@ class @MergeRequestTabs
options = $.extend({}, defaults, options)
$.ajax(options)
+
+ # Returns diff view type
+ diffViewType: ->
+ $('.inline-parallel-buttons a.active').data('view-type')
+
+ expandViewContainer: ->
+ $('.container-fluid').removeClass('container-limited')
+
+ shrinkView: ->
+ $gutterIcon = $('.js-sidebar-toggle i')
+
+ # Wait until listeners are set
+ setTimeout( ->
+ # Only when sidebar is collapsed
+ if $gutterIcon.is('.fa-angle-double-right')
+ $gutterIcon.closest('a').trigger('click',[true])
+ , 0)
diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee
index 83434c1b9ba..b3c73ffce5d 100644
--- a/app/assets/javascripts/merge_requests.js.coffee
+++ b/app/assets/javascripts/merge_requests.js.coffee
@@ -16,7 +16,7 @@
form = $("#issue_search_form")
search = $("#issue_search").val()
$('.merge-requests-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '? '+ form.serialize()
+ issues_url = form.attr('action') + '?' + form.serialize()
$.ajax
type: "GET"
@@ -27,7 +27,7 @@
success: (data) ->
$('.merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
- History.replaceState {page: issues_url}, document.title, issues_url
+ history.replaceState {page: issues_url}, document.title, issues_url
MergeRequests.reload()
dataType: "json"
diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee
index d644d50b669..0037a3a21c2 100644
--- a/app/assets/javascripts/milestone.js.coffee
+++ b/app/assets/javascripts/milestone.js.coffee
@@ -62,14 +62,24 @@ class @Milestone
dataType: "json"
constructor: ->
+ oldMouseStart = $.ui.sortable.prototype._mouseStart
+ $.ui.sortable.prototype._mouseStart = (event, overrideHandle, noActivation) ->
+ this._trigger "beforeStart", event, this._uiHash()
+ oldMouseStart.apply this, [event, overrideHandle, noActivation]
+
@bindIssuesSorting()
@bindMergeRequestSorting()
+ @bindTabsSwitching()
bindIssuesSorting: ->
$("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable(
connectWith: ".issues-sortable-list",
dropOnEmpty: true,
items: "li:not(.ui-sort-disabled)",
+ beforeStart: (event, ui) ->
+ $(".issues-sortable-list").css "min-height", ui.item.outerHeight()
+ stop: (event, ui) ->
+ $(".issues-sortable-list").css "min-height", "0px"
update: (event, ui) ->
data = $(this).sortable("serialize")
Milestone.sortIssues(data)
@@ -94,11 +104,24 @@ class @Milestone
).disableSelection()
+ bindTabsSwitching: ->
+ $('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
+ currentTabClass = $(e.target).data('show')
+ previousTabClass = $(e.relatedTarget).data('show')
+
+ $(previousTabClass).hide()
+ $(currentTabClass).removeClass('hidden')
+ $(currentTabClass).show()
+
bindMergeRequestSorting: ->
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable(
connectWith: ".merge_requests-sortable-list",
dropOnEmpty: true,
items: "li:not(.ui-sort-disabled)",
+ beforeStart: (event, ui) ->
+ $(".merge_requests-sortable-list").css "min-height", ui.item.outerHeight()
+ stop: (event, ui) ->
+ $(".merge_requests-sortable-list").css "min-height", "0px"
update: (event, ui) ->
data = $(this).sortable("serialize")
Milestone.sortMergeRequests(data)
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
new file mode 100644
index 00000000000..0287d98b1ec
--- /dev/null
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -0,0 +1,74 @@
+class @MilestoneSelect
+ constructor: ->
+ $('.js-milestone-select').each (i, dropdown) ->
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ milestonesUrl = $dropdown.data('milestones')
+ selectedMilestone = $dropdown.data('selected')
+ showNo = $dropdown.data('show-no')
+ showAny = $dropdown.data('show-any')
+ useId = $dropdown.data('use-id')
+ defaultLabel = $dropdown.text().trim()
+
+ $dropdown.glDropdown(
+ data: (term, callback) ->
+ $.ajax(
+ url: milestonesUrl
+ ).done (data) ->
+ html = $(data)
+ data = []
+ html.find('.milestone strong a').each ->
+ link = $(@).attr('href').split('/')
+ data.push(
+ id: link[link.length - 1]
+ title: $(@).text().trim()
+ )
+
+ if showNo
+ data.unshift(
+ id: '0'
+ title: 'No Milestone'
+ )
+
+ if showAny
+ data.unshift(
+ isAny: true
+ title: 'Any Milestone'
+ )
+
+ if data.length > 2
+ data.splice 2, 0, 'divider'
+
+ callback(data)
+ filterable: true
+ search:
+ fields: ['title']
+ selectable: true
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
+ text: (milestone) ->
+ milestone.title
+ id: (milestone) ->
+ if !useId
+ if !milestone.isAny?
+ milestone.title
+ else
+ ''
+ else
+ milestone.id
+ isSelected: (milestone) ->
+ milestone.title is selectedMilestone
+ clicked: ->
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ )
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index 9e5204bfeeb..b164231e7ef 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -1,4 +1,5 @@
#= require autosave
+#= require autosize
#= require dropzone
#= require dropzone_input
#= require gfm_auto_complete
@@ -14,10 +15,14 @@ class @Notes
@last_fetched_at = last_fetched_at
@view = view
@noteable_url = document.URL
- @initRefresh()
- @setupMainTargetNoteForm()
+ @notesCountBadge ||= $(".issuable-details").find(".notes-tab .badge")
+ @basePollingInterval = 15000
+ @maxPollingSteps = 4
+
@cleanBinding()
@addBinding()
+ @setPollingInterval()
+ @setupMainTargetNoteForm()
@initTaskList()
addBinding: ->
@@ -25,18 +30,19 @@ class @Notes
$(document).on "ajax:success", ".js-main-target-form", @addNote
$(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
+ # catch note ajax errors
+ $(document).on "ajax:error", ".js-main-target-form", @addNoteError
+
# change note in UI after update
- $(document).on "ajax:success", "form.edit_note", @updateNote
+ $(document).on "ajax:success", "form.edit-note", @updateNote
# Edit note link
$(document).on "click", ".js-note-edit", @showEditForm
$(document).on "click", ".note-edit-cancel", @cancelEdit
# Reopen and close actions for Issue/MR combined with note form submit
- $(document).on "click", ".js-note-target-reopen", @targetReopen
- $(document).on "click", ".js-note-target-close", @targetClose
$(document).on "click", ".js-comment-button", @updateCloseButton
- $(document).on "keyup", ".js-note-text", @updateTargetButtons
+ $(document).on "keyup input", ".js-note-text", @updateTargetButtons
# remove a note (in general)
$(document).on "click", ".js-note-delete", @removeNote
@@ -48,6 +54,9 @@ class @Notes
$(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton
$(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm
+ # reset main target form when clicking discard
+ $(document).on "click", ".js-note-discard", @resetMainTargetForm
+
# update the file name when an attachment is selected
$(document).on "change", ".js-note-attachment-input", @updateFormAttachment
@@ -63,10 +72,13 @@ class @Notes
# fetch notes when tab becomes visible
$(document).on "visibilitychange", @visibilityChange
+ # when issue status changes, we need to refresh data
+ $(document).on "issuable:change", @refresh
+
cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form"
- $(document).off "ajax:success", "form.edit_note"
+ $(document).off "ajax:success", "form.edit-note"
$(document).off "click", ".js-note-edit"
$(document).off "click", ".note-edit-cancel"
$(document).off "click", ".js-note-delete"
@@ -79,6 +91,7 @@ class @Notes
$(document).off "keyup", ".js-note-text"
$(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close"
+ $(document).off "click", ".js-note-discard"
$('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container'
@@ -87,10 +100,12 @@ class @Notes
clearInterval(Notes.interval)
Notes.interval = setInterval =>
@refresh()
- , 15000
+ , @pollingInterval
refresh: ->
- unless document.hidden or (@noteable_url != document.URL)
+ return if @refreshing is true
+ refreshing = true
+ if not document.hidden and document.URL.indexOf(@noteable_url) is 0
@getContent()
getContent: ->
@@ -101,9 +116,31 @@ class @Notes
success: (data) =>
notes = data.notes
@last_fetched_at = data.last_fetched_at
+ @setPollingInterval(data.notes.length)
$.each notes, (i, note) =>
- @renderNote(note)
+ if note.discussion_with_diff_html?
+ @renderDiscussionNote(note)
+ else
+ @renderNote(note)
+ always: =>
+ @refreshing = false
+ ###
+ Increase @pollingInterval up to 120 seconds on every function call,
+ if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
+ will reset to @basePollingInterval.
+
+ Note: this function is used to gradually increase the polling interval
+ if there aren't new notes coming from the server
+ ###
+ setPollingInterval: (shouldReset = true) ->
+ nthInterval = @basePollingInterval * Math.pow(2, @maxPollingSteps - 1)
+ if shouldReset
+ @pollingInterval = @basePollingInterval
+ else if @pollingInterval < nthInterval
+ @pollingInterval *= 2
+
+ @initRefresh()
###
Render note in main comments area.
@@ -117,18 +154,21 @@ class @Notes
flash.pinTo('.header-content')
return
+ if note.award
+ awards_handler.addAwardToEmojiBar(note.note)
+ awards_handler.scrollToAwards()
+
# render note if it not present in loaded list
# or skip if rendered
- if @isNewNote(note) && !note.award
+ else if @isNewNote(note)
@note_ids.push(note.id)
- $('ul.main-notes-list').
- append(note.html).
- syntaxHighlight()
+
+ $('ul.main-notes-list')
+ .append(note.html)
+ .syntaxHighlight()
@initTaskList()
+ @updateNotesCount(1)
- if note.award
- awards_handler.addAwardToEmojiBar(note.note)
- awards_handler.scrollToAwards()
###
Check if note does not exists on page
@@ -145,34 +185,39 @@ class @Notes
Note: for rendering inline notes use renderDiscussionNote
###
renderDiscussionNote: (note) ->
+ return unless @isNewNote(note)
+
@note_ids.push(note.id)
- form = $("form[rel='" + note.discussion_id + "']")
+ form = $("#new-discussion-note-form-#{note.discussion_id}")
row = form.closest("tr")
note_html = $(note.html)
note_html.syntaxHighlight()
# is this the first note of discussion?
- if row.is(".js-temp-notes-holder")
+ discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']")
+ if discussionContainer.length is 0
# insert the note and the reply button after the temp row
row.after note.discussion_html
# remove the note (will be added again below)
row.next().find(".note").remove()
+ # Before that, the container didn't exist
+ discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']")
+
# Add note to 'Changes' page discussions
- $(".notes[rel='" + note.discussion_id + "']").append note_html
+ discussionContainer.append note_html
# Init discussion on 'Discussion' page if it is merge request page
- if $('body').attr('data-page').indexOf('projects:merge_request') == 0
- discussion_html = $(note.discussion_with_diff_html)
- discussion_html.syntaxHighlight()
- $('ul.main-notes-list').append(discussion_html)
+ if $('body').attr('data-page').indexOf('projects:merge_request') is 0
+ $('ul.main-notes-list')
+ .append(note.discussion_with_diff_html)
+ .syntaxHighlight()
else
# append new note to all matching discussions
- $(".notes[rel='" + note.discussion_id + "']").append note_html
+ discussionContainer.append note_html
- # cleanup after successfully creating a diff/discussion note
- @removeDiscussionNoteForm(form)
+ @updateNotesCount(1)
###
Called in response the main target form has been successfully submitted.
@@ -181,7 +226,7 @@ class @Notes
Resets text and preview.
Resets buttons.
###
- resetMainTargetForm: ->
+ resetMainTargetForm: (e) =>
form = $(".js-main-target-form")
# remove validation errors
@@ -193,6 +238,8 @@ class @Notes
form.find(".js-note-text").data("autosave").reset()
+ @updateTargetButtons(e)
+
reenableTargetFormSubmitButton: ->
form = $(".js-main-target-form")
@@ -236,8 +283,10 @@ class @Notes
form.removeClass "js-new-note-form"
form.find('.div-dropzone').remove()
+ # hide discard button
+ form.find('.js-note-discard').hide()
+
# setup preview buttons
- form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
previewButton = form.find(".js-md-preview-button")
textarea = form.find(".js-note-text")
@@ -248,6 +297,7 @@ class @Notes
else
previewButton.removeClass("turn-on").addClass "turn-off"
+ autosize(textarea)
new Autosave textarea, [
"Note"
form.find("#note_commit_id").val()
@@ -270,6 +320,10 @@ class @Notes
addNote: (xhr, note, status) =>
@renderNote(note)
+ addNoteError: (xhr, note, status) =>
+ flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert')
+ flash.pinTo('.md-area')
+
###
Called in response to the new note form being submitted
@@ -278,6 +332,9 @@ class @Notes
addDiscussionNote: (xhr, note, status) =>
@renderDiscussionNote(note)
+ # cleanup after successfully creating a diff/discussion note
+ @removeDiscussionNoteForm($("#new-discussion-note-form-#{note.discussion_id}"))
+
###
Called in response to the edit note form being submitted
@@ -286,6 +343,7 @@ class @Notes
updateNote: (_xhr, note, _status) =>
# Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html)
+ $('.js-timeago', $html).timeago()
$html.syntaxHighlight()
$html.find('.js-task-list-container').taskList('enable')
@@ -305,22 +363,27 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-body > .note-text").hide()
note.find(".note-header").hide()
- base_form = note.find(".note-edit-form")
- form = base_form.clone().insertAfter(base_form)
- form.addClass('current-note-edit-form gfm-form')
- form.find('.div-dropzone').remove()
+ form = note.find(".note-edit-form")
+ isNewForm = form.is(':not(.gfm-form)')
+ if isNewForm
+ form.addClass('gfm-form')
+ form.addClass('current-note-edit-form')
+ form.show()
# Show the attachment delete link
note.find(".js-note-attachment-delete").show()
# Setup markdown form
- GitLab.GfmAutoComplete.setup()
- new DropzoneInput(form)
+ if isNewForm
+ GitLab.GfmAutoComplete.setup()
+ new DropzoneInput(form)
- form.show()
textarea = form.find("textarea")
textarea.focus()
+ if isNewForm
+ autosize(textarea)
+
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
# The textarea has the correct value, Chrome just won't show it unless we
# modify it, so let's clear it and re-set it!
@@ -328,7 +391,8 @@ class @Notes
textarea.val ""
textarea.val value
- disableButtonIfEmptyField textarea, form.find(".js-comment-button")
+ if isNewForm
+ disableButtonIfEmptyField textarea, form.find(".js-comment-button")
###
Called in response to clicking the edit note link
@@ -340,7 +404,9 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-body > .note-text").show()
note.find(".note-header").show()
- note.find(".current-note-edit-form").remove()
+ note.find(".current-note-edit-form")
+ .removeClass("current-note-edit-form")
+ .hide()
###
Called in response to deleting a note of any kind.
@@ -348,29 +414,32 @@ class @Notes
Removes the actual note from view.
Removes the whole discussion if the last note is being removed.
###
- removeNote: ->
- note = $(this).closest(".note")
- note_id = note.attr('id')
+ removeNote: (e) =>
+ noteId = $(e.currentTarget)
+ .closest(".note")
+ .attr("id")
- $('.note[id="' + note_id + '"]').each ->
- note = $(this)
+ # A same note appears in the "Discussion" and in the "Changes" tab, we have
+ # to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
+ # where $("#noteId") would return only one.
+ $(".note[id='#{noteId}']").each (i, el) =>
+ note = $(el)
notes = note.closest(".notes")
- count = notes.closest(".notes_holder").find(".discussion-notes-count")
# check if this is the last note for this line
if notes.find(".note").length is 1
- # for discussions
- notes.closest(".discussion").remove()
+ # "Discussions" tab
+ notes.closest(".timeline-entry").remove()
- # for diff lines
+ # "Changes" tab / commit view
notes.closest("tr").remove()
- else
- # update notes count
- count.get(0).lastChild.nodeValue = " #{notes.children().length - 1}"
note.remove()
+ # Decrement the "Discussions" counter only once
+ @updateNotesCount(-1)
+
###
Called in response to clicking the delete attachment link
@@ -410,12 +479,17 @@ class @Notes
###
setupDiscussionNoteForm: (dataHolder, form) =>
# setup note target
- form.attr "rel", dataHolder.data("discussionId")
+ form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}"
form.find("#line_type").val dataHolder.data("lineType")
form.find("#note_commit_id").val dataHolder.data("commitId")
form.find("#note_line_code").val dataHolder.data("lineCode")
form.find("#note_noteable_type").val dataHolder.data("noteableType")
form.find("#note_noteable_id").val dataHolder.data("noteableId")
+ form.find('.js-note-discard')
+ .show()
+ .removeClass('js-note-discard')
+ .addClass('js-close-discussion-note-form')
+ .text(form.find('.js-close-discussion-note-form').data('cancel-text'))
@setupNoteForm form
form.find(".js-note-text").focus()
form.addClass "js-discussion-note-form"
@@ -512,32 +586,55 @@ class @Notes
visibilityChange: =>
@refresh()
- targetReopen: (e) =>
- @submitNoteForm($(e.target).parents('form'))
-
- targetClose: (e) =>
- @submitNoteForm($(e.target).parents('form'))
-
- submitNoteForm: (form) =>
- noteText = form.find(".js-note-text").val()
- if noteText.trim().length > 0
- form.submit()
-
updateCloseButton: (e) =>
textarea = $(e.target)
form = textarea.parents('form')
- form.find('.js-note-target-close').text('Close')
+ closebtn = form.find('.js-note-target-close')
+ closebtn.text(closebtn.data('original-text'))
updateTargetButtons: (e) =>
textarea = $(e.target)
form = textarea.parents('form')
+ reopenbtn = form.find('.js-note-target-reopen')
+ closebtn = form.find('.js-note-target-close')
+ discardbtn = form.find('.js-note-discard')
if textarea.val().trim().length > 0
- form.find('.js-note-target-reopen').text('Comment & reopen')
- form.find('.js-note-target-close').text('Comment & close')
+ reopentext = reopenbtn.data('alternative-text')
+ closetext = closebtn.data('alternative-text')
+
+ if reopenbtn.text() isnt reopentext
+ reopenbtn.text(reopentext)
+
+ if closebtn.text() isnt closetext
+ closebtn.text(closetext)
+
+ if reopenbtn.is(':not(.btn-comment-and-reopen)')
+ reopenbtn.addClass('btn-comment-and-reopen')
+
+ if closebtn.is(':not(.btn-comment-and-close)')
+ closebtn.addClass('btn-comment-and-close')
+
+ if discardbtn.is(':hidden')
+ discardbtn.show()
else
- form.find('.js-note-target-reopen').text('Reopen')
- form.find('.js-note-target-close').text('Close')
+ reopentext = reopenbtn.data('original-text')
+ closetext = closebtn.data('original-text')
+
+ if reopenbtn.text() isnt reopentext
+ reopenbtn.text(reopentext)
+
+ if closebtn.text() isnt closetext
+ closebtn.text(closetext)
+
+ if reopenbtn.is(':not(.btn-comment-and-reopen)')
+ reopenbtn.removeClass('btn-comment-and-reopen')
+
+ if closebtn.is(':not(.btn-comment-and-close)')
+ closebtn.removeClass('btn-comment-and-close')
+
+ if discardbtn.is(':visible')
+ discardbtn.hide()
initTaskList: ->
@enableTaskList()
@@ -548,3 +645,6 @@ class @Notes
updateTaskList: ->
$('form', this).submit()
+
+ updateNotesCount: (updateCount) ->
+ @notesCountBadge.text(parseInt(@notesCountBadge.text()) + updateCount)
diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee
index d639303aed3..0ff83b7f0c8 100644
--- a/app/assets/javascripts/pager.js.coffee
+++ b/app/assets/javascripts/pager.js.coffee
@@ -1,6 +1,7 @@
@Pager =
init: (@limit = 0, preload, @disable = false) ->
- @loading = $(".loading")
+ @loading = $('.loading').first()
+
if preload
@offset = 0
@getOld()
diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee
index bb0b66b86e1..20f87440551 100644
--- a/app/assets/javascripts/profile.js.coffee
+++ b/app/assets/javascripts/profile.js.coffee
@@ -4,12 +4,13 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit()
- $('.update-username form').on 'ajax:before', ->
- $('.loading-gif').show()
+ $('.update-username').on 'ajax:before', ->
+ $('.loading-username').show()
$(this).find('.update-success').hide()
$(this).find('.update-failed').hide()
- $('.update-username form').on 'ajax:complete', ->
+ $('.update-username').on 'ajax:complete', ->
+ $('.loading-username').hide()
$(this).find('.btn-save').enable()
$(this).find('.loading-gif').hide()
@@ -24,3 +25,12 @@ class @Profile
form = $(this).closest("form")
filename = $(this).val().replace(/^.*[\\\/]/, '')
form.find(".js-avatar-filename").text(filename)
+
+$ ->
+ # Extract the SSH Key title from its comment
+ $(document).on 'focusout.ssh_key', '#key_key', ->
+ $title = $('#key_title')
+ comment = $(@).val().match(/^\S+ \S+ (.+)\n?$/)
+
+ if comment && comment.length > 1 && $title.val() == ''
+ $title.val(comment[1]).change()
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
index d7a658f8faa..76bc4ff42a2 100644
--- a/app/assets/javascripts/project.js.coffee
+++ b/app/assets/javascripts/project.js.coffee
@@ -50,3 +50,19 @@ class @Project
$('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>")
$(@).parents('ul').find('li.active').removeClass 'active'
$(@).parent().addClass 'active'
+
+ @projectSelectDropdown()
+
+ projectSelectDropdown: ->
+ new ProjectSelect()
+
+ $('.project-item-select').on 'click', (e) =>
+ @changeProject $(e.currentTarget).val()
+
+ $('.js-projects-dropdown-toggle').on 'click', (e) ->
+ e.preventDefault()
+
+ $('.js-projects-dropdown').select2('open')
+
+ changeProject: (url) ->
+ window.location = url
diff --git a/app/assets/javascripts/project_find_file.js.coffee b/app/assets/javascripts/project_find_file.js.coffee
new file mode 100644
index 00000000000..0dd32352c34
--- /dev/null
+++ b/app/assets/javascripts/project_find_file.js.coffee
@@ -0,0 +1,125 @@
+class @ProjectFindFile
+ constructor: (@element, @options)->
+ @filePaths = {}
+ @inputElement = @element.find(".file-finder-input")
+
+ # init event
+ @initEvent()
+
+ # focus text input box
+ @inputElement.focus()
+
+ # load file list
+ @load(@options.url)
+
+ # init event
+ initEvent: ->
+ @inputElement.off "keyup"
+ @inputElement.on "keyup", (event) =>
+ target = $(event.target)
+ value = target.val()
+ oldValue = target.data("oldValue") ? ""
+
+ if value != oldValue
+ target.data("oldValue", value)
+ @findFile()
+ @element.find("tr.tree-item").eq(0).addClass("selected").focus()
+
+ @element.find(".tree-content-holder .tree-table").on "click", (event) ->
+ if (event.target.nodeName != "A")
+ path = @element.find(".tree-item-file-name a", this).attr("href")
+ location.href = path if path
+
+ # find file
+ findFile: ->
+ searchText = @inputElement.val()
+ result = if searchText.length > 0 then fuzzaldrinPlus.filter(@filePaths, searchText) else @filePaths
+ @renderList result, searchText
+
+ # files pathes load
+ load: (url) ->
+ $.ajax
+ url: url
+ method: "get"
+ dataType: "json"
+ success: (data) =>
+ @element.find(".loading").hide()
+ @filePaths = data
+ @findFile()
+ @element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus()
+
+ # render result
+ renderList: (filePaths, searchText) ->
+ @element.find(".tree-table > tbody").empty()
+
+ for filePath, i in filePaths
+ break if i == 20
+
+ if searchText
+ matches = fuzzaldrinPlus.match(filePath, searchText)
+
+ blobItemUrl = "#{@options.blobUrlTemplate}/#{filePath}"
+
+ html = @makeHtml filePath, matches, blobItemUrl
+ @element.find(".tree-table > tbody").append(html)
+
+ # highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
+ highlighter = (element, text, matches) ->
+ lastIndex = 0
+ highlightText = ""
+ matchedChars = []
+
+ for matchIndex in matches
+ unmatched = text.substring(lastIndex, matchIndex)
+
+ if unmatched
+ element.append(matchedChars.join("").bold()) if matchedChars.length
+ matchedChars = []
+ element.append(document.createTextNode(unmatched))
+
+ matchedChars.push(text[matchIndex])
+ lastIndex = matchIndex + 1
+
+ element.append(matchedChars.join("").bold()) if matchedChars.length
+ element.append(document.createTextNode(text.substring(lastIndex)))
+
+ # make tbody row html
+ makeHtml: (filePath, matches, blobItemUrl) ->
+ $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>")
+ if matches
+ $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl))
+ else
+ $tr.find("a").attr("href", blobItemUrl).text(filePath)
+
+ return $tr
+
+ selectRow: (type) ->
+ rows = @element.find(".files-slider tr.tree-item")
+ selectedRow = @element.find(".files-slider tr.tree-item.selected")
+
+ if rows && rows.length > 0
+ if selectedRow && selectedRow.length > 0
+ if type == "UP"
+ next = selectedRow.prev()
+ else if type == "DOWN"
+ next = selectedRow.next()
+
+ if next.length > 0
+ selectedRow.removeClass "selected"
+ selectedRow = next
+ else
+ selectedRow = rows.eq(0)
+ selectedRow.addClass("selected").focus()
+
+ selectRowUp: =>
+ @selectRow "UP"
+
+ selectRowDown: =>
+ @selectRow "DOWN"
+
+ goToTree: =>
+ location.href = @options.treeUrl
+
+ goToBlob: =>
+ path = @element.find(".tree-item.selected .tree-item-file-name a").attr("href")
+ location.href = path if path
diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee
index fecdb9fc2e7..63dee4ed5d7 100644
--- a/app/assets/javascripts/project_new.js.coffee
+++ b/app/assets/javascripts/project_new.js.coffee
@@ -3,3 +3,16 @@ class @ProjectNew
$('.project-edit-container').on 'ajax:before', =>
$('.project-edit-container').hide()
$('.save-project-loader').show()
+ @toggleSettings()
+ @toggleSettingsOnclick()
+
+
+ toggleSettings: ->
+ checked = $("#project_builds_enabled").prop("checked")
+ if checked
+ $('.builds-feature').show()
+ else
+ $('.builds-feature').hide()
+
+ toggleSettingsOnclick: ->
+ $("#project_builds_enabled").on 'click', @toggleSettings
diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee
index 0ae274f3363..be8ab9b428d 100644
--- a/app/assets/javascripts/project_select.js.coffee
+++ b/app/assets/javascripts/project_select.js.coffee
@@ -3,6 +3,7 @@ class @ProjectSelect
$('.ajax-project-select').each (i, select) ->
@groupId = $(select).data('group-id')
@includeGroups = $(select).data('include-groups')
+ @orderBy = $(select).data('order-by') || 'id'
placeholder = "Search for project"
placeholder += " or group" if @includeGroups
@@ -28,7 +29,7 @@ class @ProjectSelect
if @groupId
Api.groupProjects @groupId, query.term, projectsCallback
else
- Api.projects query.term, projectsCallback
+ Api.projects query.term, @orderBy, projectsCallback
id: (project) ->
project.web_url
diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee
index f2887af190b..e4c4bf3b273 100644
--- a/app/assets/javascripts/projects_list.js.coffee
+++ b/app/assets/javascripts/projects_list.js.coffee
@@ -1,24 +1,37 @@
-class @ProjectsList
- constructor: ->
- $(".projects-list .js-expand").on 'click', (e) ->
- e.preventDefault()
- list = $(this).closest('.projects-list')
- list.find("li").show()
- list.find("li.bottom").hide()
+@ProjectsList =
+ init: ->
+ $(".projects-list-filter").off('keyup')
+ this.initSearch()
+ this.initPagination()
- $(".projects-list-filter").keyup ->
- terms = $(this).val()
- uiBox = $('div.projects-list-holder')
- if terms == "" || terms == undefined
- uiBox.find("ul.projects-list li").show()
- else
- uiBox.find("ul.projects-list li").each (index) ->
- name = $(this).find("span.filter-title").text()
+ initSearch: ->
+ @timer = null
+ $(".projects-list-filter").on('keyup', ->
+ clearTimeout(@timer)
+ @timer = setTimeout(ProjectsList.filterResults, 500)
+ )
- if name.toLowerCase().search(terms.toLowerCase()) == -1
- $(this).hide()
- else
- $(this).show()
- uiBox.find("ul.projects-list li.bottom").hide()
+ filterResults: =>
+ $('.projects-list-holder').fadeTo(250, 0.5)
+ form = null
+ form = $("form#project-filter-form")
+ search = $(".projects-list-filter").val()
+ project_filter_url = form.attr('action') + '?' + form.serialize()
+ $.ajax
+ type: "GET"
+ url: form.attr('action')
+ data: form.serialize()
+ complete: ->
+ $('.projects-list-holder').fadeTo(250, 1)
+ success: (data) ->
+ $('.projects-list-holder').replaceWith(data.html)
+ # Change url so if user reload a page - search results are saved
+ history.replaceState {page: project_filter_url}, document.title, project_filter_url
+ dataType: "json"
+
+ initPagination: ->
+ $('.projects-list-holder .pagination').on('ajax:success', (e, data) ->
+ $('.projects-list-holder').replaceWith(data.html)
+ )
diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee
index 4d915bfc8c5..100e3aac535 100644
--- a/app/assets/javascripts/shortcuts.js.coffee
+++ b/app/assets/javascripts/shortcuts.js.coffee
@@ -4,16 +4,23 @@ class @Shortcuts
Mousetrap.reset()
Mousetrap.bind('?', @selectiveHelp)
Mousetrap.bind('s', Shortcuts.focusSearch)
+ Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
+ Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL?
selectiveHelp: (e) =>
Shortcuts.showHelp(e, @enabledHelp)
+ toggleMarkdownPreview: (e) =>
+ $(document).triggerHandler('markdown-preview:toggle', [e])
+
@showHelp: (e, location) ->
if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show')
else
+ url = '/help/shortcuts'
+ url = gon.relative_url_root + url if gon.relative_url_root?
$.ajax(
- url: '/help/shortcuts',
+ url: url,
dataType: 'script',
success: (e) ->
if location and location.length > 0
@@ -32,3 +39,14 @@ $(document).on 'click.more_help', '.js-more-help-button', (e) ->
$(@).remove()
$('.hidden-shortcut').show()
e.preventDefault()
+
+Mousetrap.stopCallback = (->
+ defaultStopCallback = Mousetrap.stopCallback
+
+ return (e, element, combo) ->
+ # allowed shortcuts if textarea, input, contenteditable are focused
+ if ['ctrl+shift+p', 'command+shift+p'].indexOf(combo) != -1
+ return false
+ else
+ return defaultStopCallback.apply(@, arguments)
+)()
diff --git a/app/assets/javascripts/shortcuts_find_file.js.coffee b/app/assets/javascripts/shortcuts_find_file.js.coffee
new file mode 100644
index 00000000000..311e80bae19
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_find_file.js.coffee
@@ -0,0 +1,19 @@
+#= require shortcuts_navigation
+
+class @ShortcutsFindFile extends ShortcutsNavigation
+ constructor: (@projectFindFile) ->
+ super()
+ _oldStopCallback = Mousetrap.stopCallback
+ # override to fire shortcuts action when focus in textbox
+ Mousetrap.stopCallback = (event, element, combo) =>
+ if element == @projectFindFile.inputElement[0] and (combo == 'up' or combo == 'down' or combo == 'esc' or combo == 'enter')
+ # when press up/down key in textbox, cusor prevent to move to home/end
+ event.preventDefault()
+ return false
+
+ return _oldStopCallback(event, element, combo)
+
+ Mousetrap.bind('up', @projectFindFile.selectRowUp)
+ Mousetrap.bind('down', @projectFindFile.selectRowDown)
+ Mousetrap.bind('esc', @projectFindFile.goToTree)
+ Mousetrap.bind('enter', @projectFindFile.goToBlob)
diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee
index bb532194682..bbf02f1db24 100644
--- a/app/assets/javascripts/shortcuts_issuable.coffee
+++ b/app/assets/javascripts/shortcuts_issuable.coffee
@@ -5,23 +5,46 @@ class @ShortcutsIssuable extends ShortcutsNavigation
constructor: (isMergeRequest) ->
super()
Mousetrap.bind('a', ->
- $('.js-assignee').select2('open')
+ $('.block.assignee .edit-link').trigger('click')
return false
)
Mousetrap.bind('m', ->
- $('.js-milestone').select2('open')
+ $('.block.milestone .edit-link').trigger('click')
return false
)
Mousetrap.bind('r', =>
@replyWithSelectedText()
return false
)
+ Mousetrap.bind('j', =>
+ @prevIssue()
+ return false
+ )
+ Mousetrap.bind('k', =>
+ @nextIssue()
+ return false
+ )
+ Mousetrap.bind('e', =>
+ @editIssue()
+ return false
+ )
+
if isMergeRequest
@enabledHelp.push('.hidden-shortcut.merge_requests')
else
@enabledHelp.push('.hidden-shortcut.issues')
+ prevIssue: ->
+ $prevBtn = $('.prev-btn')
+ if not $prevBtn.hasClass('disabled')
+ Turbolinks.visit($prevBtn.attr('href'))
+
+ nextIssue: ->
+ $nextBtn = $('.next-btn')
+ if not $nextBtn.hasClass('disabled')
+ Turbolinks.visit($nextBtn.attr('href'))
+
replyWithSelectedText: ->
if window.getSelection
selected = window.getSelection().toString()
@@ -44,3 +67,7 @@ class @ShortcutsIssuable extends ShortcutsNavigation
# Focus the input field
replyField.focus()
+
+ editIssue: ->
+ $editBtn = $('.issuable-edit')
+ Turbolinks.visit($editBtn.attr('href'))
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index ae59480af9e..eea3f5ee910 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -1,11 +1,27 @@
-$(document).on("click", '.toggle-nav-collapse', (e) ->
- e.preventDefault()
- collapsed = 'page-sidebar-collapsed'
- expanded = 'page-sidebar-expanded'
+collapsed = 'page-sidebar-collapsed'
+expanded = 'page-sidebar-expanded'
+toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
+
+ setTimeout ( ->
+ niceScrollBars = $('.nicescroll').niceScroll();
+ niceScrollBars.updateScrollBar();
+ ), 300
+
+$(document).on("click", '.toggle-nav-collapse', (e) ->
+ e.preventDefault()
+
+ toggleSidebar()
)
+
+$ ->
+ size = bp.getBreakpointSize()
+
+ if size is "xs" or size is "sm"
+ if $('.page-with-sidebar').hasClass(expanded)
+ toggleSidebar()
diff --git a/app/assets/javascripts/star.js.coffee b/app/assets/javascripts/star.js.coffee
index d849b2e7950..f27780dda93 100644
--- a/app/assets/javascripts/star.js.coffee
+++ b/app/assets/javascripts/star.js.coffee
@@ -6,7 +6,7 @@ class @Star
$starIcon = $this.find('i')
toggleStar = (isStarred) ->
- $this.parent().find('span.count').text data.star_count
+ $this.parent().find('.star-count').text data.star_count
if isStarred
$starSpan.removeClass('starred').text 'Star'
$starIcon.removeClass('fa-star').addClass 'fa-star-o'
@@ -19,4 +19,4 @@ class @Star
return
).on 'ajax:error', (e, xhr, status, error) ->
new Flash('Star toggle failed. Try again later.', 'alert')
- return \ No newline at end of file
+ return
diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee
index 7f41616d4e7..084f0e0dc65 100644
--- a/app/assets/javascripts/subscription.js.coffee
+++ b/app/assets/javascripts/subscription.js.coffee
@@ -1,17 +1,21 @@
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")
+ constructor: (container) ->
+ $container = $(container)
+ @url = $container.attr('data-url')
+ @subscribe_button = $container.find('.subscribe-button')
+ @subscription_status = $container.find('.subscription-status')
+ @subscribe_button.unbind('click').click(@toggleSubscription)
-
+ toggleSubscription: (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.find('>div').toggleClass('hidden')
diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee
new file mode 100644
index 00000000000..b6b4bd90e6a
--- /dev/null
+++ b/app/assets/javascripts/todos.js.coffee
@@ -0,0 +1,56 @@
+class @Todos
+ constructor: (@name) ->
+ @clearListeners()
+ @initBtnListeners()
+
+ clearListeners: ->
+ $('.done-todo').off('click')
+ $('.js-todos-mark-all').off('click')
+
+ initBtnListeners: ->
+ $('.done-todo').on('click', @doneClicked)
+ $('.js-todos-mark-all').on('click', @allDoneClicked)
+
+ doneClicked: (e) =>
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ $this = $(e.currentTarget)
+ $this.disable()
+
+ $.ajax
+ type: 'POST'
+ url: $this.attr('href')
+ dataType: 'json'
+ data: '_method': 'delete'
+ success: (data) =>
+ @clearDone $this.closest('li')
+ @updateBadges data
+
+ allDoneClicked: (e) =>
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ $this = $(e.currentTarget)
+ $this.disable()
+
+ $.ajax
+ type: 'POST'
+ url: $this.attr('href')
+ dataType: 'json'
+ data: '_method': 'delete'
+ success: (data) =>
+ $this.remove()
+ $('.js-todos-list').remove()
+ @updateBadges data
+
+ clearDone: ($row) ->
+ $ul = $row.closest('ul')
+ $row.remove()
+
+ if not $ul.find('li').length
+ $ul.parents('.panel').remove()
+
+ updateBadges: (data) ->
+ $('.todos-pending .badge, .todos-pending-count').text data.count
+ $('.todos-done .badge').text data.done_count
diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee
index ec4271b092c..2882a90d118 100644
--- a/app/assets/javascripts/user.js.coffee
+++ b/app/assets/javascripts/user.js.coffee
@@ -1,10 +1,17 @@
class @User
- constructor: ->
+ constructor: (@opts) ->
$('.profile-groups-avatars').tooltip("placement": "top")
- new ProjectsList()
+
+ @initTabs()
$('.hide-project-limit-message').on 'click', (e) ->
path = '/'
$.cookie('hide_project_limit_message', 'false', { path: path })
$(@).parents('.project-limit-message').remove()
e.preventDefault()
+
+ initTabs: ->
+ new UserTabs(
+ parentEl: '.user-profile'
+ action: @opts.action
+ )
diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee
new file mode 100644
index 00000000000..09b7eec9104
--- /dev/null
+++ b/app/assets/javascripts/user_tabs.js.coffee
@@ -0,0 +1,146 @@
+# UserTabs
+#
+# Handles persisting and restoring the current tab selection and lazily-loading
+# content on the Users#show page.
+#
+# ### Example Markup
+#
+# <ul class="nav-links">
+# <li class="activity-tab active">
+# <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
+# Activity
+# </a>
+# </li>
+# <li class="groups-tab">
+# <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
+# Groups
+# </a>
+# </li>
+# <li class="contributed-tab">
+# <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
+# Contributed projects
+# </a>
+# </li>
+# <li class="projects-tab">
+# <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
+# Personal projects
+# </a>
+# </li>
+# </ul>
+#
+# <div class="tab-content">
+# <div class="tab-pane" id="activity">
+# Activity Content
+# </div>
+# <div class="tab-pane" id="groups">
+# Groups Content
+# </div>
+# <div class="tab-pane" id="contributed">
+# Contributed projects content
+# </div>
+# <div class="tab-pane" id="projects">
+# Projects content
+# </div>
+# </div>
+#
+# <div class="loading-status">
+# <div class="loading">
+# Loading Animation
+# </div>
+# </div>
+#
+class @UserTabs
+ constructor: (opts) ->
+ {
+ @action = 'activity'
+ @defaultAction = 'activity'
+ @parentEl = $(document)
+ } = opts
+
+ # Make jQuery object if selector is provided
+ @parentEl = $(@parentEl) if typeof @parentEl is 'string'
+
+ # Store the `location` object, allowing for easier stubbing in tests
+ @_location = location
+
+ # Set tab states
+ @loaded = {}
+ for item in @parentEl.find('.nav-links a')
+ @loaded[$(item).attr 'data-action'] = false
+
+ # Actions
+ @actions = Object.keys @loaded
+
+ @bindEvents()
+
+ # Set active tab
+ @action = @defaultAction if @action is 'show'
+ @activateTab(@action)
+
+ bindEvents: ->
+ # Toggle event listeners
+ @parentEl
+ .off 'shown.bs.tab', '.nav-links a[data-toggle="tab"]'
+ .on 'shown.bs.tab', '.nav-links a[data-toggle="tab"]', @tabShown
+
+ tabShown: (event) =>
+ $target = $(event.target)
+ action = $target.data('action')
+ source = $target.attr('href')
+
+ @setTab(source, action)
+ @setCurrentAction(action)
+
+ activateTab: (action) ->
+ @parentEl.find(".nav-links .#{action}-tab a").tab('show')
+
+ setTab: (source, action) ->
+ return if @loaded[action] is true
+
+ if action is 'activity'
+ @loadActivities(source)
+
+ if action in ['groups', 'contributed', 'projects']
+ @loadTab(source, action)
+
+ loadTab: (source, action) ->
+ $.ajax
+ beforeSend: => @toggleLoading(true)
+ complete: => @toggleLoading(false)
+ dataType: 'json'
+ type: 'GET'
+ url: "#{source}.json"
+ success: (data) =>
+ tabSelector = 'div#' + action
+ @parentEl.find(tabSelector).html(data.html)
+ @loaded[action] = true
+
+ loadActivities: (source) ->
+ return if @loaded['activity'] is true
+
+ $calendarWrap = @parentEl.find('.user-calendar')
+ $calendarWrap.load($calendarWrap.data('href'))
+
+ new Activities()
+ @loaded['activity'] = true
+
+ toggleLoading: (status) ->
+ @parentEl.find('.loading-status .loading').toggle(status)
+
+ setCurrentAction: (action) ->
+ # Remove possible actions from URL
+ regExp = new RegExp('\/(' + @actions.join('|') + ')(\.html)?\/?$')
+ new_state = @_location.pathname
+ new_state = new_state.replace(/\/+$/, "") # remove trailing slashes
+ new_state = new_state.replace(regExp, '')
+
+ # Append the new action if we're on a tab other than 'activity'
+ unless action == @defaultAction
+ new_state += "/#{action}"
+
+ # Ensure parameters and hash come along for the ride
+ new_state += @_location.search + @_location.hash
+
+ history.replaceState {turbolinks: true, url: new_state}, document.title, new_state
+
+ new_state
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 9467011799f..48831dd6bc4 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -3,6 +3,94 @@ class @UsersSelect
@usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json"
+ $('.js-user-search').each (i, dropdown) =>
+ $dropdown = $(dropdown)
+ @projectId = $dropdown.data('project-id')
+ @showCurrentUser = $dropdown.data('current-user')
+ showNullUser = $dropdown.data('null-user')
+ showAnyUser = $dropdown.data('any-user')
+ firstUser = $dropdown.data('first-user')
+ selectedId = $dropdown.data('selected')
+ defaultLabel = $dropdown.text().trim()
+
+ $dropdown.glDropdown(
+ data: (term, callback) =>
+ @users term, (users) =>
+ if term.length is 0
+ showDivider = 0
+
+ if firstUser
+ # Move current user to the front of the list
+ for obj, index in users
+ if obj.username == firstUser
+ users.splice(index, 1)
+ users.unshift(obj)
+ break
+
+ if showNullUser
+ showDivider += 1
+ users.unshift(
+ name: 'Unassigned',
+ id: 0
+ )
+
+ if showAnyUser
+ showDivider += 1
+ name = showAnyUser
+ name = 'Any User' if name == true
+ anyUser = {
+ name: name,
+ id: null
+ }
+ users.unshift(anyUser)
+
+ if showDivider
+ users.splice(showDivider, 0, "divider")
+
+ # Send the data back
+ callback users
+ filterable: true
+ filterRemote: true
+ search:
+ fields: ['name', 'username']
+ selectable: true
+ fieldName: $dropdown.data('field-name')
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.name
+ else
+ defaultLabel
+ clicked: ->
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ renderRow: (user) ->
+ username = if user.username then "@#{user.username}" else ""
+ avatar = if user.avatar_url then user.avatar_url else false
+ selected = if user.id is selectedId then "is-active" else ""
+ img = ""
+
+ if avatar
+ img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
+
+ "<li>
+ <a href='#' class='dropdown-menu-user-link #{selected}'>
+ #{img}
+ <strong class='dropdown-menu-user-full-name'>
+ #{user.name}
+ </strong>
+ <span class='dropdown-menu-user-username'>
+ #{username}
+ </span>
+ </a>
+ </li>"
+ )
+
$('.ajax-users-select').each (i, select) =>
@projectId = $(select).data('project-id')
@groupId = $(select).data('group-id')
diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee
index 81cfc37b956..1ee827f1fa3 100644
--- a/app/assets/javascripts/wikis.js.coffee
+++ b/app/assets/javascripts/wikis.js.coffee
@@ -1,17 +1,19 @@
+#= require latinise
+
class @Wikis
constructor: ->
- $('.build-new-wiki').bind "click", (e) ->
- $('[data-error~=slug]').addClass("hidden")
- $('p.hint').show()
+ $('.new-wiki-page').on 'submit', (e) =>
+ $('[data-error~=slug]').addClass('hidden')
field = $('#new_wiki_path')
- valid_slug_pattern = /^[\w\/-]+$/
+ slug = @slugify(field.val())
- slug = field.val()
- if slug.match valid_slug_pattern
+ if (slug.length > 0)
path = field.attr('data-wikis-path')
- if(slug.length > 0)
- location.href = path + "/" + slug
- else
+ location.href = path + '/' + slug
e.preventDefault()
- $('p.hint').hide()
- $('[data-error~=slug]').removeClass("hidden")
+
+ dasherize: (value) ->
+ value.replace(/[_\s]+/g, '-')
+
+ slugify: (value) =>
+ @dasherize(value.trim().toLowerCase().latinise())
diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee
index a1462cf3cae..e1c5446eaac 100644
--- a/app/assets/javascripts/zen_mode.js.coffee
+++ b/app/assets/javascripts/zen_mode.js.coffee
@@ -1,56 +1,80 @@
+# Zen Mode (full screen) textarea
+#
+#= provides zen_mode:enter
+#= provides zen_mode:leave
+#
+#= require jquery.scrollTo
#= require dropzone
#= require mousetrap
#= require mousetrap/pause
-
+#
+# ### Events
+#
+# `zen_mode:enter`
+#
+# Fired when the "Edit in fullscreen" link is clicked.
+#
+# **Synchronicity** Sync
+# **Bubbles** Yes
+# **Cancelable** No
+# **Target** a.js-zen-enter
+#
+# `zen_mode:leave`
+#
+# Fired when the "Leave Fullscreen" link is clicked.
+#
+# **Synchronicity** Sync
+# **Bubbles** Yes
+# **Cancelable** No
+# **Target** a.js-zen-leave
+#
class @ZenMode
constructor: ->
- @active_zen_area = null
- @active_checkbox = null
- @scroll_position = 0
-
- $(window).scroll =>
- if not @active_checkbox
- @scroll_position = window.pageYOffset
+ @active_backdrop = null
+ @active_textarea = null
- $('body').on 'click', '.zen-enter-link', (e) =>
+ $(document).on 'click', '.js-zen-enter', (e) ->
e.preventDefault()
- $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', true).change()
+ $(e.currentTarget).trigger('zen_mode:enter')
- $('body').on 'click', '.zen-leave-link', (e) =>
+ $(document).on 'click', '.js-zen-leave', (e) ->
e.preventDefault()
- $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', false).change()
-
- $('body').on 'change', '.zen-toggle-comment', (e) =>
- checkbox = e.currentTarget
- if checkbox.checked
- # Disable other keyboard shortcuts in ZEN mode
- Mousetrap.pause()
- @updateActiveZenArea(checkbox)
- else
- @exitZenMode()
-
- $(document).on 'keydown', (e) =>
- if e.keyCode is 27 # Esc
- @exitZenMode()
+ $(e.currentTarget).trigger('zen_mode:leave')
+
+ $(document).on 'zen_mode:enter', (e) =>
+ @enter(e.target.parentNode)
+ $(document).on 'zen_mode:leave', (e) =>
+ @exit()
+
+ $(document).on 'keydown', (e) ->
+ if e.keyCode == 27 # Esc
e.preventDefault()
+ $(document).trigger('zen_mode:leave')
+
+ enter: (backdrop) ->
+ Mousetrap.pause()
+
+ @active_backdrop = $(backdrop)
+ @active_backdrop.addClass('fullscreen')
+
+ @active_textarea = @active_backdrop.find('textarea')
- updateActiveZenArea: (checkbox) =>
- @active_checkbox = $(checkbox)
- @active_checkbox.prop('checked', true)
- @active_zen_area = @active_checkbox.parent().find('textarea')
# Prevent a user-resized textarea from persisting to fullscreen
- @active_zen_area.removeAttr('style')
- @active_zen_area.focus()
+ @active_textarea.removeAttr('style')
+ @active_textarea.focus()
- exitZenMode: =>
- if @active_zen_area isnt null
+ exit: ->
+ if @active_textarea
Mousetrap.unpause()
- @active_checkbox.prop('checked', false)
- @active_zen_area = null
- @active_checkbox = null
- @restoreScroll(@scroll_position)
- # Enable dropzone when leaving ZEN mode
+
+ @active_textarea.closest('.zen-backdrop').removeClass('fullscreen')
+
+ @scrollTo(@active_textarea)
+
+ @active_textarea = null
+ @active_backdrop = null
+
Dropzone.forElement('.div-dropzone').enable()
- restoreScroll: (y) ->
- window.scrollTo(window.pageXOffset, y)
+ scrollTo: (zen_area) ->
+ $.scrollTo(zen_area, 0, offset: -150)
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 0c0451fe4dd..2d301d21ab9 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -25,12 +25,6 @@
@import "framework";
/*
- * NProgress load bar css
- */
-@import 'nprogress';
-@import 'nprogress-bootstrap';
-
-/*
* Font icons
*/
@import "font-awesome";
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 48a4971c8fc..c85ab9148d0 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -11,6 +11,7 @@
@import "framework/calendar.scss";
@import "framework/callout.scss";
@import "framework/common.scss";
+@import "framework/dropdowns.scss";
@import "framework/files.scss";
@import "framework/filters.scss";
@import "framework/flash.scss";
@@ -24,7 +25,9 @@
@import "framework/lists.scss";
@import "framework/markdown_area.scss";
@import "framework/mobile.scss";
+@import "framework/nav.scss";
@import "framework/pagination.scss";
+@import "framework/progress.scss";
@import "framework/panels.scss";
@import "framework/selects.scss";
@import "framework/sidebar.scss";
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 36e582d4854..b7ffa3e6ffb 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -24,6 +24,7 @@
&.s26 { width: 26px; height: 26px; margin-right: 8px; }
&.s32 { width: 32px; height: 32px; margin-right: 10px; }
&.s36 { width: 36px; height: 36px; margin-right: 10px; }
+ &.s40 { width: 40px; height: 40px; margin-right: 10px; }
&.s46 { width: 46px; height: 46px; margin-right: 15px; }
&.s48 { width: 48px; height: 48px; margin-right: 10px; }
&.s60 { width: 60px; height: 60px; margin-right: 12px; }
@@ -40,7 +41,8 @@
&.s16 { font-size: 12px; line-height: 1.33; }
&.s24 { font-size: 14px; line-height: 1.8; }
&.s26 { font-size: 20px; line-height: 1.33; }
- &.s32 { font-size: 22px; line-height: 32px; }
+ &.s32 { font-size: 20px; line-height: 32px; }
+ &.s40 { font-size: 16px; line-height: 40px; }
&.s60 { font-size: 32px; line-height: 60px; }
&.s90 { font-size: 36px; line-height: 90px; }
&.s110 { font-size: 40px; line-height: 112px; font-weight: 300; }
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 206d39cc9b3..c36f29dda0e 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -18,12 +18,12 @@
line-height: 36px;
}
-.content-block,
.gray-content-block {
- margin: -$gl-padding;
+ margin-top: 0;
+ margin-bottom: -$gl-padding;
background-color: $background-color;
padding: $gl-padding;
- margin-bottom: 0px;
+ margin-bottom: 0;
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
color: $gl-gray;
@@ -66,21 +66,27 @@
}
.oneline {
- line-height: 42px;
+ line-height: 35px;
}
> p:last-child {
margin-bottom: 0;
}
+
+ .block-controls {
+ float: right;
+
+ .control {
+ float: left;
+ margin-left: 10px;
+ }
+ }
}
.cover-block {
text-align: center;
background: $background-color;
- margin: -$gl-padding;
- margin-bottom: 0;
- padding: 44px $gl-padding;
- border-bottom: 1px solid $border-color;
+ padding-top: 44px;
position: relative;
.avatar-holder {
@@ -110,6 +116,10 @@
.cover-desc {
padding: 0 $gl-padding 3px;
color: $gl-text-color;
+
+ &.username:last-child {
+ padding-bottom: $gl-padding;
+ }
}
.cover-controls {
@@ -127,3 +137,27 @@
.block-connector {
margin-top: -1px;
}
+
+.nav-block {
+ .controls {
+ float: right;
+ margin-top: 11px;
+ }
+}
+
+.content-block {
+ padding: $gl-padding 0;
+ border-bottom: 1px solid $border-color;
+
+ &.oneline-block {
+ line-height: 36px;
+ }
+
+ > .controls {
+ float: right;
+ }
+}
+
+.content-block-small {
+ padding: 10px 0;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 97a94638847..657c5f033c7 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,24 +1,18 @@
@mixin btn-default {
@include border-radius(3px);
- border-width: 1px;
- border-style: solid;
- font-size: 15px;
+ font-size: $gl-font-size;
font-weight: 500;
- line-height: 18px;
- padding: 11px $gl-padding;
- letter-spacing: .4px;
+ padding: $gl-vert-padding $gl-btn-padding;
&:focus,
&:active {
outline: none;
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+ @include box-shadow($gl-btn-active-background);
}
}
@mixin btn-middle {
@include btn-default;
- @include border-radius(3px);
- padding: 11px 24px;
}
@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
@@ -34,7 +28,7 @@
}
&:active {
- @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12));
+ @include box-shadow ($gl-btn-active-background);
background-color: $dark;
border-color: $border-dark;
@@ -43,23 +37,23 @@
}
@mixin btn-green {
- @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #FFFFFF);
+ @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #fff);
}
@mixin btn-blue {
- @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF);
+ @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #fff);
}
@mixin btn-blue-medium {
- @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #FFFFFF);
+ @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #fff);
}
@mixin btn-orange {
- @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF);
+ @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #fff);
}
@mixin btn-red {
- @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #FFFFFF);
+ @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #fff);
}
@mixin btn-gray {
@@ -74,23 +68,27 @@
@include btn-default;
@include btn-white;
- &.btn-sm {
- padding: 5px 10px;
+ color: $gl-text-color;
+
+ &:focus:active {
+ outline: 0;
}
- &.btn-nr {
- padding: 7px 10px;
+ &.btn-small,
+ &.btn-sm {
+ padding: 4px 10px;
+ font-size: 13px;
+ line-height: 18px;
}
&.btn-xs {
- padding: 1px 5px;
+ padding: 2px 5px;
}
&.btn-success,
&.btn-new,
&.btn-create,
- &.btn-save,
- &.btn-green {
+ &.btn-save {
@include btn-green;
}
@@ -129,8 +127,31 @@
margin-right: 7px;
float: left;
&:last-child {
- margin-right: 0px;
+ margin-right: 0;
}
+ &.btn-xs {
+ margin-right: 3px;
+ }
+ }
+ &.disabled {
+ pointer-events: auto !important;
+ }
+
+ .caret {
+ margin-left: 5px;
+ }
+}
+
+.btn-transparent {
+ color: $btn-transparent-color;
+ background-color: transparent;
+ border: 0;
+
+ &:hover,
+ &:active,
+ &:focus {
+ background-color: transparent;
+ box-shadow: none;
}
}
@@ -148,38 +169,52 @@
margin-right: 7px;
float: left;
&:last-child {
- margin-right: 0px;
+ margin-right: 0;
}
}
}
-.btn-group-next {
+.btn-clipboard {
+ border: none;
+ padding: 0 5px;
+}
+
+.input-group-btn {
.btn {
- padding: 9px 0px;
- font-size: 15px;
- color: #7f8fa4;
- border-color: #e7e9ed;
- width: 140px;
-
- .badge {
- font-weight: normal;
- background-color: #eee;
- color: #78a;
+ @include btn-middle;
+
+ &:hover {
+ outline: none;
}
- &.active {
- border-color: $gl-info;
- background: $gl-info;
- color: #fff;
+ &:focus {
+ outline: none;
+ }
+
+ &:active {
+ outline: none;
+ }
- .badge {
- color: $gl-info;
- background-color: white;
- }
+ &.btn-clipboard {
+ padding-left: 15px;
+ padding-right: 15px;
}
}
+
+ .active {
+ @include box-shadow($gl-btn-active-background);
+
+ border: 1px solid #c6cacf !important;
+ background-color: #e4e7ed !important;
+ }
}
-.btn-clipboard {
- border: none;
+.btn-loading {
+ &:not(.disabled) .fa {
+ display: none;
+ }
+
+ .fa {
+ margin-right: 5px;
+ }
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index a36fefe22c5..e3192823a1a 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -19,38 +19,33 @@
}
}
}
+
/**
* This overwrites the default values of the cal-heatmap gem
*/
.calendar {
.qi {
- background-color: #999;
fill: #fff;
}
.q1 {
- background-color: #dae289;
- fill: #ededed;
+ fill: #ededed !important;
}
.q2 {
- background-color: #cedb9c;
- fill: #ACD5F2;
+ fill: #acd5f2 !important;
}
.q3 {
- background-color: #b5cf6b;
- fill: #7FA8D1;
+ fill: #7fa8d1 !important;
}
.q4 {
- background-color: #637939;
- fill: #49729B;
+ fill: #49729b !important;
}
.q5 {
- background-color: #3b6427;
- fill: #254E77;
+ fill: #254e77 !important;
}
.domain-background {
@@ -59,32 +54,7 @@
}
.ch-tooltip {
- position: absolute;
- display: none;
- margin-top: 22px;
- margin-left: 1px;
- font-size: 13px;
padding: 3px;
font-weight: 550;
- background-color: #222;
- span {
- position: absolute;
- width: 200px;
- text-align: center;
- visibility: hidden;
- border-radius: 10px;
- &:after {
- content: '';
- position: absolute;
- top: 100%;
- left: 50%;
- margin-left: -8px;
- width: 0;
- height: 0;
- border-top: 8px solid #000000;
- border-right: 8px solid transparent;
- border-left: 8px solid transparent;
- }
- }
}
}
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index 20a9bfb9816..da7bab74a32 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -39,6 +39,6 @@
}
.bs-callout-success {
background-color: #dff0d8;
- border-color: #5cA64d;
+ border-color: #5ca64d;
color: #3c763d;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 11730000f85..bc03c2180be 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -1,21 +1,27 @@
/** COLORS **/
.cgray { color: $gl-gray; }
-.clgray { color: #BBB }
+.clgray { color: #bbb }
.cred { color: $gl-text-red; }
.cgreen { color: $gl-text-green; }
.cdark { color: #444 }
/** COMMON CLASSES **/
-.prepend-top-10 { margin-top:10px }
+.prepend-top-0 { margin-top: 0; }
+.prepend-top-5 { margin-top: 5px; }
+.prepend-top-10 { margin-top: 10px }
.prepend-top-default { margin-top: $gl-padding !important; }
-.prepend-top-20 { margin-top:20px }
-.prepend-left-10 { margin-left:10px }
-.prepend-left-20 { margin-left:20px }
-.append-right-10 { margin-right:10px }
-.append-right-20 { margin-right:20px }
-.append-bottom-10 { margin-bottom:10px }
-.append-bottom-15 { margin-bottom:15px }
-.append-bottom-20 { margin-bottom:20px }
+.prepend-top-20 { margin-top: 20px }
+.prepend-left-10 { margin-left: 10px }
+.prepend-left-default { margin-left: $gl-padding; }
+.prepend-left-20 { margin-left: 20px }
+.append-right-5 { margin-right: 5px }
+.append-right-10 { margin-right: 10px }
+.append-right-default { margin-right: $gl-padding; }
+.append-right-20 { margin-right: 20px }
+.append-bottom-0 { margin-bottom: 0 }
+.append-bottom-10 { margin-bottom: 10px }
+.append-bottom-15 { margin-bottom: 15px }
+.append-bottom-20 { margin-bottom: 20px }
.append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block }
.center { text-align: center }
@@ -45,7 +51,7 @@ pre {
}
&.well-pre {
- border: 1px solid #EEE;
+ border: 1px solid #eee;
background: #f9f9f9;
border-radius: 0;
color: #555;
@@ -56,25 +62,12 @@ hr {
margin: $gl-padding 0;
}
-.dropdown-menu > li > a {
- text-shadow: none;
-}
-
-.dropdown-menu-align-right {
- left: auto;
- right: 0px;
-}
-
-.dropdown-menu > li > a:hover,
-.dropdown-menu > li > a:focus {
- background: $gl-primary;
- color: #FFF;
-}
-
.str-truncated {
@include str-truncated;
}
+.item-title { font-weight: 600; }
+
/** FLASH message **/
.author_link {
color: $gl-link-color;
@@ -110,7 +103,7 @@ span.update-author {
}
.user-mention {
- color: #2FA0BB;
+ color: #2fa0bb;
font-weight: bold;
}
@@ -118,14 +111,6 @@ span.update-author {
display: inline;
}
-.line_holder {
- &:hover {
- td {
- background: #FFFFCF !important;
- }
- }
-}
-
p.time {
color: #999;
font-size: 90%;
@@ -149,10 +134,10 @@ p.time {
// Fix issue with notes & lists creating a bunch of bottom borders.
li.note {
- img { max-width:100% }
+ img { max-width: 100% }
.note-title {
li {
- border-bottom:none !important;
+ border-bottom: none !important;
}
}
}
@@ -202,9 +187,9 @@ li.note {
.error-message {
padding: 10px;
- background: #C67;
+ background: #c67;
margin: 0;
- color: #FFF;
+ color: #fff;
a {
color: #fff;
@@ -215,7 +200,7 @@ li.note {
.browser-alert {
padding: 10px;
text-align: center;
- background: #C67;
+ background: #c67;
color: #fff;
font-weight: bold;
a {
@@ -286,7 +271,7 @@ img.emoji {
table {
td.permission-x {
- background: #D9EDF7 !important;
+ background: #d9edf7 !important;
text-align: center;
}
}
@@ -295,7 +280,7 @@ table {
float: left;
text-align: center;
font-size: 32px;
- color: #AAA;
+ color: #aaa;
width: 60px;
}
@@ -307,7 +292,7 @@ table {
}
.btn-sign-in {
- margin-top: 8px;
+ margin-top: 10px;
text-shadow: none;
}
@@ -317,14 +302,6 @@ table {
}
}
-.wiki .highlight, .note-body .highlight {
- margin: 12px 0 12px 0;
-}
-
-.wiki .code {
- overflow-x: auto;
-}
-
.footer-links {
margin-bottom: 20px;
a {
@@ -370,76 +347,7 @@ table {
.profiler-button,
.profiler-controls {
- border-color: #EEE !important;
- }
-}
-
-.center-top-menu, .left-top-menu {
- @include nav-menu;
- text-align: center;
- margin-top: 5px;
- margin-bottom: $gl-padding;
- height: auto;
- margin-top: -$gl-padding;
-
- &.no-bottom {
- margin-bottom: 0;
- }
-
- &.no-top {
- margin-top: 0;
- }
-
- li a {
- display: inline-block;
- padding-top: $gl-padding;
- padding-bottom: 11px;
- margin-bottom: -1px;
- }
-
- &.bottom-border {
- border-bottom: 1px solid $border-color;
- height: 57px;
- }
-
- &.wide {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- }
-}
-
-.left-top-menu {
- text-align: left;
- border-bottom: 1px solid #EEE;
-}
-
-.center-middle-menu {
- @include nav-menu;
- padding: 0;
- text-align: center;
- margin: -$gl-padding;
- margin-top: 0;
- margin-bottom: 0;
- height: 58px;
- border-bottom: 1px solid $border-color;
-
- li {
- &:after {
- content: "|";
- color: $border-gray-light;
- }
-
- &:last-child {
- &:after {
- content: none;
- }
- }
-
- > a {
- display: inline-block;
- text-transform: uppercase;
- font-size: 13px;
- }
+ border-color: #eee !important;
}
}
@@ -459,11 +367,11 @@ table {
margin-bottom: $gl-padding;
}
-.new-project-item-select-holder {
+.project-item-select-holder {
display: inline-block;
position: relative;
- .new-project-item-select {
+ .project-item-select {
position: absolute;
top: 0;
right: 0;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
new file mode 100644
index 00000000000..a48b6c17fa0
--- /dev/null
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -0,0 +1,348 @@
+.caret {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin-left: 2px;
+ vertical-align: middle;
+ border-top: $caret-width-base dashed;
+ border-right: $caret-width-base solid transparent;
+ border-left: $caret-width-base solid transparent;
+}
+
+.btn-group {
+ .caret {
+ margin-left: 0;
+ }
+}
+
+.dropdown {
+ position: relative;
+}
+
+.open {
+ .dropdown-menu {
+ display: block;
+ }
+
+ .dropdown-menu-toggle {
+ border-color: $dropdown-toggle-hover-border-color;
+
+ .fa {
+ color: $dropdown-toggle-hover-icon-color;
+ }
+ }
+}
+
+.dropdown-menu-toggle {
+ position: relative;
+ width: 160px;
+ padding: 6px 20px 6px 10px;
+ background-color: $dropdown-toggle-bg;
+ color: $dropdown-toggle-color;
+ font-size: 15px;
+ text-align: left;
+ border: 1px solid $dropdown-toggle-border-color;
+ border-radius: 2px;
+ outline: 0;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+
+ .fa {
+ position: absolute;
+ top: 50%;
+ right: 6px;
+ margin-top: -4px;
+ color: $dropdown-toggle-icon-color;
+ font-size: 10px;
+ }
+
+ &:hover, {
+ border-color: $dropdown-toggle-hover-border-color;
+
+ .fa {
+ color: $dropdown-toggle-hover-icon-color;
+ }
+ }
+}
+
+.dropdown-menu {
+ display: none;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 9;
+ width: 240px;
+ margin-top: 2px;
+ margin-bottom: 0;
+ padding: 10px 10px;
+ font-size: 14px;
+ font-weight: normal;
+ background-color: $dropdown-bg;
+ border: 1px solid $dropdown-border-color;
+ border-radius: $border-radius-base;
+ box-shadow: 0 2px 4px $dropdown-shadow-color;
+
+ &.is-loading {
+ .dropdown-content {
+ display: none;
+ }
+
+ .dropdown-loading {
+ display: block;
+ }
+ }
+
+ ul {
+ margin: 0;
+ padding: 0;
+ }
+
+ li {
+ text-align: left;
+ list-style: none;
+ }
+
+ .divider {
+ width: 100%;
+ height: 1px;
+ margin-top: 8px;
+ margin-bottom: 8px;
+ background-color: $dropdown-divider-color;
+ }
+
+ a {
+ display: block;
+ position: relative;
+ padding-left: 10px;
+ padding-right: 10px;
+ color: $dropdown-link-color;
+ line-height: 34px;
+ text-overflow: ellipsis;
+ border-radius: 2px;
+ white-space: nowrap;
+ overflow: hidden;
+
+ &:hover,
+ &:focus,
+ &.is-focused {
+ background-color: $dropdown-link-hover-bg;
+ text-decoration: none;
+ outline: 0;
+ }
+ }
+}
+
+.dropdown-menu-paging {
+ .dropdown-page-two,
+ .dropdown-menu-back {
+ display: none;
+ }
+
+ &.is-page-two {
+ .dropdown-page-one {
+ display: none;
+ }
+
+ .dropdown-page-two,
+ .dropdown-menu-back {
+ display: block;
+ }
+ }
+}
+
+.dropdown-menu-user {
+ .avatar {
+ float: left;
+ width: 30px;
+ height: 30px;
+ margin: 0 10px 0 0;
+ }
+}
+
+.dropdown-menu-user-link {
+ padding-top: 7px;
+ padding-bottom: 7px;
+}
+
+.dropdown-menu-user-full-name {
+ display: block;
+ font-weight: 600;
+ line-height: 16px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.dropdown-menu-user-username {
+ display: block;
+ line-height: 16px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.dropdown-select {
+ width: 280px;
+}
+
+.dropdown-menu-align-right {
+ left: auto;
+ right: 0;
+}
+
+.dropdown-menu-selectable {
+ a {
+ padding-left: 25px;
+
+ &.is-active {
+ &::before {
+ content: "\f00c";
+ position: absolute;
+ left: 5px;
+ top: 50%;
+ margin-top: -7px;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ }
+ }
+}
+
+.dropdown-header {
+ padding-left: 5px;
+ padding-right: 5px;
+ color: $dropdown-header-color;
+ font-size: 13px;
+ line-height: 22px;
+}
+
+.dropdown-title {
+ position: relative;
+ margin-bottom: 10px;
+ padding-left: 30px;
+ padding-right: 30px;
+ padding-bottom: 10px;
+ font-weight: 600;
+ line-height: 1;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-bottom: 1px solid $dropdown-divider-color;
+ overflow: hidden;
+}
+
+.dropdown-title-button {
+ position: absolute;
+ top: -1px;
+ padding: 0;
+ color: $dropdown-title-btn-color;
+ font-size: 14px;
+ border: 0;
+ background: none;
+ outline: 0;
+
+ &:hover {
+ color: darken($dropdown-title-btn-color, 15%);
+ }
+}
+
+.dropdown-menu-close {
+ right: 0;
+}
+
+.dropdown-menu-back {
+ left: 0;
+}
+
+.dropdown-input {
+ position: relative;
+ margin-bottom: 10px;
+
+ .fa {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ color: #c7c7c7;
+ font-size: 12px;
+ pointer-events: none;
+ }
+}
+
+.dropdown-input-field {
+ width: 100%;
+ padding: 0 7px;
+ color: $dropdown-input-color;
+ line-height: 30px;
+ border: 1px solid $dropdown-divider-color;
+ border-radius: 2px;
+ outline: 0;
+
+ &:focus {
+ color: $dropdown-link-color;
+ border-color: $dropdown-input-focus-border;
+ box-shadow: 0 0 4px $dropdown-input-focus-shadow;
+
+ + .fa {
+ color: $dropdown-link-color;
+ }
+ }
+
+ &:hover {
+ + .fa {
+ color: $dropdown-link-color;
+ }
+ }
+}
+
+.dropdown-content {
+ max-height: 215px;
+ overflow-y: scroll;
+}
+
+.dropdown-footer {
+ padding-top: 10px;
+ margin-top: 10px;
+ font-size: 13px;
+ border-top: 1px solid $dropdown-divider-color;
+}
+
+.dropdown-footer-list {
+ font-size: 14px;
+
+ a {
+ padding-left: 10px;
+ }
+}
+
+.dropdown-loading {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: none;
+ z-index: 9;
+ background-color: $dropdown-loading-bg;
+ font-size: 28px;
+
+ .fa {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-top: -14px;
+ margin-left: -14px;
+ }
+}
+
+.dropdown-menu-labels {
+ .label {
+ position: relative;
+ width: 30px;
+ margin-right: 5px;
+ text-indent: -99999px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index cbfd4bc29b6..646e2610831 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -3,13 +3,11 @@
*
*/
.file-holder {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
border: none;
- border-top: 1px solid #E7E9EE;
- border-bottom: 1px solid #E7E9EE;
+ border: 1px solid $border-color;
&.readme-holder {
+ margin-top: 10px;
border-bottom: 0;
}
@@ -32,12 +30,26 @@
right: 15px;
.btn {
- padding: 0px 10px;
+ padding: 0 10px;
font-size: 13px;
line-height: 28px;
}
}
+ .filename {
+ &.old {
+ span.idiff {
+ background-color: #f8cbcb;
+ }
+ }
+
+ &.new {
+ span.idiff {
+ background-color: #a6f3a6;
+ }
+ }
+ }
+
.left-options {
margin-top: -3px;
}
@@ -72,7 +84,7 @@
&.blob-no-preview {
background: #eee;
- text-shadow: 0 1px 2px #FFF;
+ text-shadow: 0 1px 2px #fff;
padding: 100px 0;
}
@@ -95,15 +107,6 @@
&:last-child {
border-right: none;
}
- background: #fff;
- }
- .lines {
- pre {
- padding: 0;
- margin: 0;
- background: none;
- border: none;
- }
}
img.avatar {
border: 0 none;
@@ -119,18 +122,18 @@
color: #888;
}
}
- td.blame-numbers {
- pre {
- color: #AAA;
- white-space: pre;
- }
- background: #f1f1f1;
- border-left: 1px solid #DDD;
+ td.line-numbers {
+ float: none;
+ border-left: 1px solid #ddd;
}
td.lines {
+ padding: 0;
code {
font-family: $monospace_font;
}
+ pre {
+ margin: 0;
+ }
}
}
@@ -155,7 +158,7 @@
}
&:hover {
- background: $hover;
+ background: $row-hover;
}
}
}
@@ -166,6 +169,19 @@
*/
&.code {
padding: 0;
+ -webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
}
}
}
+
+span.idiff {
+ &.left {
+ border-top-left-radius: 2px;
+ border-bottom-left-radius: 2px;
+ }
+
+ &.right {
+ border-top-right-radius: 2px;
+ border-bottom-right-radius: 2px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 8e6922c9231..40a508c1ebc 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -1,30 +1,13 @@
.filter-item {
- margin-right: 15px;
+ margin-right: 6px;
+ vertical-align: top;
}
-@media (min-width: 800px) {
+@media (min-width: $screen-sm-min) {
.issues-filters,
.issues_bulk_update {
- select, .select2-container {
- width: 120px !important;
- display: inline-block;
+ .dropdown-menu-toggle {
+ width: 132px;
}
}
}
-
-@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/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 82eb50ad4be..1bfd0213995 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -8,10 +8,12 @@
.flash-notice {
@extend .alert;
@extend .alert-info;
+ margin: 0;
}
.flash-alert {
@extend .alert;
@extend .alert-danger;
+ margin: 0;
}
}
diff --git a/app/assets/stylesheets/framework/fonts.scss b/app/assets/stylesheets/framework/fonts.scss
index e214567eca1..7a946109e3a 100644
--- a/app/assets/stylesheets/framework/fonts.scss
+++ b/app/assets/stylesheets/framework/fonts.scss
@@ -3,23 +3,39 @@
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
- src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), font-url('SourceSansPro-Light.ttf');
+ src:
+ local('Source Sans Pro Light'),
+ local('SourceSansPro-Light'),
+ font-url('SourceSansPro-Light.ttf.woff2') format('woff2'),
+ font-url('SourceSansPro-Light.ttf.woff') format('woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
- src: local('Source Sans Pro'), local('SourceSansPro-Regular'), font-url('SourceSansPro-Regular.ttf');
+ src:
+ local('Source Sans Pro'),
+ local('SourceSansPro-Regular'),
+ font-url('SourceSansPro-Regular.ttf.woff2') format('woff2'),
+ font-url('SourceSansPro-Regular.ttf.woff') format('woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
- src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), font-url('SourceSansPro-Semibold.ttf');
+ src:
+ local('Source Sans Pro Semibold'),
+ local('SourceSansPro-Semibold'),
+ font-url('SourceSansPro-Semibold.ttf.woff2') format('woff2'),
+ font-url('SourceSansPro-Semibold.ttf.woff') format('woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
- src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), font-url('SourceSansPro-Bold.ttf');
+ src:
+ local('Source Sans Pro Bold'),
+ local('SourceSansPro-Bold'),
+ font-url('SourceSansPro-Bold.ttf.woff2') format('woff2'),
+ font-url('SourceSansPro-Bold.ttf.woff') format('woff');
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 032d343df44..4cb4129b71b 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -2,16 +2,47 @@ textarea {
resize: vertical;
}
-input[type='search'].search-text-input {
- background-image: image-url("icon-search.png");
+input {
+ border-radius: $border-radius-base;
+}
+
+input[type='search'] {
+ background-color: white;
+ padding-left: 10px;
+}
+
+input[type='search'].search-input {
background-repeat: no-repeat;
background-position: 10px;
- padding-left: 25px;
+ background-size: 16px;
+ background-position-x: 30%;
+ padding-left: 10px;
+ background-color: $gray-light;
+
+ &.search-input[value=""] {
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC');
+ }
+
+ &.search-input::-webkit-input-placeholder {
+ text-align: center;
+ }
+
+ &.search-input:-moz-placeholder { /* Firefox 18- */
+ text-align: center;
+ }
+
+ &.search-input::-moz-placeholder { /* Firefox 19+ */
+ text-align: center;
+ }
+
+ &.search-input:-ms-input-placeholder {
+ text-align: center;
+ }
}
input[type='text'].danger {
- background: #F2DEDE!important;
- border-color: #D66;
+ background: #f2dede!important;
+ border-color: #d66;
text-shadow: 0 1px 1px #fff
}
@@ -38,6 +69,10 @@ label {
&.inline-label {
margin: 0;
}
+
+ &.label-light {
+ font-weight: 600;
+ }
}
.inline-input-group {
@@ -74,8 +109,11 @@ label {
.form-control {
@include box-shadow(none);
- height: 42px;
- padding: 8px $gl-padding;
+ border-radius: 3px;
+}
+
+.form-control-inline {
+ display: inline;
}
.wiki-content {
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 8d9a0aae568..2a4cf4fc335 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -23,13 +23,13 @@
&:hover {
background-color: $color-darker;
a {
- color: #FFF;
+ color: #fff;
}
}
}
.collapse-nav a {
- color: #FFF;
+ color: #fff;
background: $color;
}
@@ -42,7 +42,7 @@
&:hover {
background-color: $color-dark;
- color: #FFF;
+ color: #fff;
text-decoration: none;
}
}
@@ -71,7 +71,7 @@
}
&.active a {
- color: #FFF;
+ color: #fff;
background: $color-dark;
&.no-highlight {
@@ -79,42 +79,42 @@
}
i {
- color: #FFF
+ color: #fff
}
}
}
}
}
-$theme-blue: #2980B9;
+$theme-blue: #2980b9;
$theme-charcoal: #333c47;
-$theme-graphite: #888888;
+$theme-graphite: #888;
$theme-gray: #373737;
$theme-green: #019875;
-$theme-violet: #554488;
+$theme-violet: #548;
body {
&.ui_blue {
- @include gitlab-theme(#BECDE9, $theme-blue, #1970A9, #096099);
+ @include gitlab-theme(#becde9, $theme-blue, #1970a9, #096099);
}
&.ui_charcoal {
- @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272D);
+ @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272d);
}
&.ui_graphite {
- @include gitlab-theme(#CCCCCC, $theme-graphite, #777777, #666666);
+ @include gitlab-theme(#ccc, $theme-graphite, #777, #666);
}
&.ui_gray {
- @include gitlab-theme(#979797, $theme-gray, #272727, #222222);
+ @include gitlab-theme(#979797, $theme-gray, #272727, #222);
}
&.ui_green {
- @include gitlab-theme(#AADDCC, $theme-green, #018865, #017855);
+ @include gitlab-theme(#adc, $theme-green, #018865, #017855);
}
&.ui_violet {
- @include gitlab-theme(#9988CC, $theme-violet, #443366, #332255);
+ @include gitlab-theme(#98c, $theme-violet, #436, #325);
}
-}
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 4dbbb56104b..71a7ecab8ef 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -7,8 +7,8 @@ header {
&.navbar-empty {
height: 58px;
- background: #FFF;
- border-bottom: 1px solid #EEE;
+ background: #fff;
+ border-bottom: 1px solid #eee;
.center-logo {
margin: 11px 0;
@@ -28,6 +28,7 @@ header {
min-height: $header-height;
background-color: #fff;
border: none;
+ border-bottom: 1px solid #eee;
.container-fluid {
width: 100% !important;
@@ -46,7 +47,7 @@ header {
text-align: center;
&:hover, &:focus, &:active {
- background-color: #FFF;
+ background-color: #fff;
}
}
@@ -58,7 +59,7 @@ header {
right: 2px;
&:hover {
- background-color: #EEE;
+ background-color: #eee;
}
&.active {
color: #7f8fa4;
@@ -72,11 +73,11 @@ header {
.title {
margin: 0;
- overflow: hidden;
font-size: 19px;
line-height: $header-height;
font-weight: normal;
color: #4c4e54;
+ overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
@@ -87,6 +88,22 @@ header {
text-decoration: underline;
}
}
+
+ .dropdown-toggle-caret {
+ position: relative;
+ top: -2px;
+ width: 12px;
+ line-height: 12px;
+ margin-left: 5px;
+ font-size: 10px;
+ text-align: center;
+ cursor: pointer;
+ }
+
+ .project-item-select {
+ right: auto;
+ left: 0;
+ }
}
.navbar-collapse {
@@ -107,16 +124,10 @@ header {
.search-input {
width: 220px;
- background-image: image-url("icon-search.png");
- background-repeat: no-repeat;
- background-position: 195px;
- @include input-big;
&:focus {
@include box-shadow(none);
outline: none;
- border-color: #DDD;
- background-color: #FFF;
}
}
}
@@ -130,18 +141,18 @@ header {
margin-left: $sidebar_collapsed_width;
}
-@media (max-width: $screen-md-max) {
- .header-collapsed, .header-expanded {
+.header-collapsed {
+ margin-left: $sidebar_collapsed_width;
+
+ @media (min-width: $screen-md-min) {
@include collapsed-header;
}
}
-@media(min-width: $screen-md-max) {
- .header-collapsed {
- @include collapsed-header;
- }
+.header-expanded {
+ margin-left: $sidebar_collapsed_width;
- .header-expanded {
+ @media (min-width: $screen-md-min) {
margin-left: $sidebar_width;
}
}
@@ -151,7 +162,7 @@ header {
font-size: 18px;
.navbar-nav {
- margin: 0px;
+ margin: 0;
float: none !important;
.visible-xs, .visable-sm {
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 2e13ee842e0..7cf4d4fba42 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -1,8 +1,8 @@
.file-content.code {
border: none;
box-shadow: none;
- margin: 0px;
- padding: 0px;
+ margin: 0;
+ padding: 0;
table-layout: fixed;
pre {
@@ -17,6 +17,7 @@
overflow-y: hidden;
white-space: pre;
word-wrap: normal;
+ border-left: 1px solid;
code {
font-family: $monospace_font;
@@ -25,7 +26,7 @@
padding: 0;
.line {
- display: inline;
+ display: inline-block;
}
}
}
@@ -43,8 +44,10 @@
white-space: nowrap;
i {
+ float: left;
+ margin-top: 3px;
+ margin-right: 5px;
visibility: hidden;
- @extend .pull-left;
}
&:hover i {
@@ -53,18 +56,3 @@
}
}
}
-
-.note-text .code {
- border: none;
- box-shadow: none;
- background: $background-color;
- padding: 1em;
- overflow-x: auto;
-
- code {
- font-family: $monospace_font;
- white-space: pre;
- word-wrap: normal;
- padding: 0;
- }
-}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index e93dbab0c42..7f7b7c806e7 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -5,32 +5,38 @@
*/
.status-box {
- @include border-radius(3px);
+
+ /* Extra small devices (phones, less than 768px) */
+ /* No media query since this is the default in Bootstrap */
+ padding: 5px 11px;
+ margin-top: 4px;
+ /* Small devices (tablets, 768px and up) */
+ @media (min-width: $screen-sm-min) {
+ padding: 0 $gl-btn-padding;
+ margin-top: 5px;
+ }
+ @include border-radius(3px);
display: block;
float: left;
- padding: 0 $gl-padding;
- font-weight: normal;
margin-right: 10px;
+ color: #fff;
font-size: $gl-font-size;
+ line-height: 25px;
&.status-box-closed {
background-color: $gl-danger;
- color: #FFF;
}
&.status-box-merged {
background-color: $gl-primary;
- color: #FFF;
}
&.status-box-open {
background-color: $green-light;
- color: #FFF;
}
&.status-box-expired {
background: #cea61b;
- color: #FFF;
}
}
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index 871b808bad4..525ed81b059 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -3,13 +3,13 @@
font-size: $font-size-base;
&.ui-datepicker-inline {
- border: 1px solid #DDD;
+ border: 1px solid #ddd;
padding: 10px;
width: 270px;
.ui-datepicker-header {
- background: #FFF;
- border-color: #DDD;
+ background: #fff;
+ border-color: #ddd;
}
.ui-datepicker-calendar td a {
@@ -19,7 +19,7 @@
}
&.ui-autocomplete {
- border-color: #DDD;
+ border-color: #ddd;
padding: 0;
margin-top: 2px;
z-index: 1001;
@@ -30,26 +30,37 @@
}
.ui-state-default {
- border: 1px solid #FFF;
- background: #FFF;
+ border: 1px solid #fff;
+ background: #fff;
color: #777;
}
.ui-state-highlight {
- border: 1px solid #EEE;
- background: #EEE;
+ border: 1px solid #eee;
+ background: #eee;
}
.ui-state-active {
border: 1px solid $gl-primary;
background: $gl-primary;
- color: #FFF;
+ color: #fff;
}
.ui-state-hover,
.ui-state-focus {
- border: 1px solid $hover;
- background: $hover;
+ border: 1px solid $row-hover;
+ background: $row-hover;
color: #333;
}
}
+
+.ui-sortable-handle {
+ cursor: move;
+ cursor: -webkit-grab;
+ cursor: -moz-grab;
+
+ &:active {
+ cursor: -webkit-grabbing;
+ cursor: -moz-grabbing;
+ }
+}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index a1a9990241d..e901c78d02f 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -5,8 +5,6 @@ html {
}
body {
- background-color: #F3F3F3 !important;
-
&.navless {
background-color: white !important;
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 1c74e525a60..2b4bb1eebf9 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -3,6 +3,7 @@
*
*/
.well-list {
+ position: relative;
margin: 0;
padding: 0;
list-style: none;
@@ -38,7 +39,7 @@
&.smoke { background-color: $background-color; }
&:hover {
- background: $hover;
+ background: $row-hover;
}
&:last-child {
@@ -74,7 +75,7 @@
/** light list with border-bottom between li **/
-ul.bordered-list {
+ul.bordered-list, ul.unstyled-list {
@include basic-list;
&.top-list {
@@ -88,6 +89,10 @@ ul.bordered-list {
}
}
+ul.unstyled-list > li {
+ border-bottom: none;
+}
+
ul.task-list {
li.task-list-item {
list-style-type: none;
@@ -105,11 +110,21 @@ ul.content-list {
padding: 0;
> li {
- padding: $gl-padding;
border-color: $table-border-color;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- color: $gl-gray;
+ color: $list-text-color;
+ font-size: $list-font-size;
+
+ .title {
+ color: $list-title-color;
+ font-weight: 600;
+ }
+
+ .description {
+ p {
+ @include str-truncated;
+ margin-bottom: 0;
+ }
+ }
.avatar {
margin-right: 15px;
@@ -126,10 +141,8 @@ ul.content-list {
}
}
-.panel > .content-list {
- li {
- margin: 0;
- }
+.panel > .content-list > li {
+ padding: $gl-padding-top $gl-padding;
}
ul.controls {
@@ -144,7 +157,7 @@ ul.controls {
> li {
float: left;
margin-right: 10px;
-
+
&:last-child {
margin-right: 0;
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 4a00a197d9a..8328aac4e7a 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -65,48 +65,25 @@
position: relative;
}
-.md-header {
- ul {
- float: left;
- margin-bottom: 1px;
- }
-}
-
.referenced-users {
color: #4c4e54;
padding-top: 10px;
}
.md-preview-holder {
- background: #FFF;
+ background: #fff;
border: 1px solid #ddd;
min-height: 169px;
padding: 5px;
box-shadow: none;
}
-.new_note,
-.edit_note,
-.detail-page-description,
-.milestone-description,
-.wiki-content,
-.merge-request-form {
- .nav-tabs {
- margin-bottom: 0;
- border: none;
-
- li a,
- li.active a {
- border: 1px solid #DDD;
- }
- }
-}
-
.markdown-area {
@include border-radius(0);
- background: #FFF;
+ background: #fff;
border: 1px solid #ddd;
min-height: 140px;
+ max-height: 500px;
padding: 5px;
box-shadow: none;
width: 100%;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 41fd890f14f..377bfa174bd 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -67,17 +67,17 @@
* Base mixin for lists in GitLab
*/
@mixin basic-list {
- margin: 5px 0px;
- padding: 0px;
+ margin: 5px 0;
+ padding: 0;
list-style: none;
> li {
@include clearfix;
padding: 10px 0;
- border-bottom: 1px solid #EEE;
+ border-bottom: 1px solid #eee;
display: block;
- margin: 0px;
+ margin: 0;
&:last-child {
border-bottom: none;
@@ -118,38 +118,3 @@
font-size: 16px;
line-height: 24px;
}
-
-@mixin nav-menu {
- padding: 0;
- margin: 0;
- list-style: none;
- height: 56px;
-
- li {
- display: inline-block;
-
- a {
- padding: 14px;
- font-size: 15px;
- line-height: 28px;
- color: #959494;
- border-bottom: 2px solid transparent;
-
- &:hover, &:active, &:focus {
- text-decoration: none;
- outline: none;
- }
- }
-
- &.active a {
- color: #616060;
- border-bottom: 2px solid #4688f1;
- }
-
- .badge {
- font-weight: normal;
- background-color: #eee;
- color: #78a;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index c00709fb6bb..5ea4f9a49db 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -9,7 +9,7 @@
padding-right: 5px;
}
- .nav.nav-tabs > li > a {
+ .nav-links > li > a {
padding: 10px;
font-size: 12px;
margin-right: 3px;
@@ -81,7 +81,7 @@
display: none;
}
- .center-top-menu, .left-top-menu {
+ .nav-links, .nav-links {
li a {
font-size: 14px;
padding: 19px 10px;
@@ -100,11 +100,6 @@
}
@media (max-width: $screen-sm-max) {
- .page-with-sidebar .content-wrapper {
- padding: 0;
- padding-top: 1px;
- }
-
.issues-filters {
.milestone-filter, .labels-filter {
display: none;
@@ -121,7 +116,7 @@
display: none;
}
- aside {
+ aside:not(.right-sidebar){
display: none;
}
@@ -133,12 +128,12 @@
.show-aside {
display: none;
position: fixed;
- right: 0px;
+ right: 0;
top: 30%;
padding: 5px 15px;
- background: #EEE;
+ background: #eee;
font-size: 20px;
color: #777;
z-index: 100;
- @include box-shadow(0 1px 2px #DDD);
+ @include box-shadow(0 1px 2px #ddd);
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
new file mode 100644
index 00000000000..5f4ce87b085
--- /dev/null
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -0,0 +1,142 @@
+.nav-links {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ height: auto;
+ border-bottom: 1px solid $border-color;
+
+ li {
+ display: inline-block;
+
+ a {
+ display: inline-block;
+ padding: 14px;
+ padding-top: $gl-padding;
+ padding-bottom: 11px;
+ margin-bottom: -1px;
+ font-size: 15px;
+ line-height: 28px;
+ color: #959494;
+ border-bottom: 2px solid transparent;
+
+ &:hover, &:active, &:focus {
+ text-decoration: none;
+ outline: none;
+ }
+ }
+
+ &.active a {
+ color: #000;
+ border-bottom: 2px solid #4688f1;
+ }
+
+ .badge {
+ font-weight: normal;
+ background-color: #eee;
+ color: #78a;
+ }
+ }
+}
+
+.top-area {
+ @include clearfix;
+
+ border-bottom: 1px solid #eee;
+
+ .nav-text {
+ padding-top: 16px;
+ padding-bottom: 11px;
+ display: inline-block;
+ width: 50%;
+ line-height: 28px;
+
+ /* Small devices (phones, tablets, 768px and lower) */
+ @media (max-width: $screen-sm-min) {
+ width: 100%;
+ }
+ }
+
+ .nav-links {
+ display: inline-block;
+ width: 50%;
+ margin-bottom: 0;
+ border-bottom: none;
+
+ /* Small devices (phones, tablets, 768px and lower) */
+ @media (max-width: $screen-sm-max) {
+ width: 100%;
+ }
+ }
+
+ .nav-controls {
+ width: 50%;
+ display: inline-block;
+ float: right;
+ text-align: right;
+ padding: 11px 0;
+ margin-bottom: 0;
+
+ > .dropdown {
+ margin-right: $gl-padding-top;
+ display: inline-block;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ > .btn {
+ margin-right: $gl-padding-top;
+ display: inline-block;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ > .btn-grouped {
+ float: none;
+ }
+
+ > form {
+ display: inline-block;
+ }
+
+ input {
+ height: 34px;
+ display: inline-block;
+ position: relative;
+ top: 1px;
+ margin-right: $gl-padding-top;
+
+ /* Medium devices (desktops, 992px and up) */
+ @media (min-width: $screen-md-min) { width: 200px; }
+
+ /* Large devices (large desktops, 1200px and up) */
+ @media (min-width: $screen-lg-min) { width: 250px; }
+
+ &.input-short {
+ /* Medium devices (desktops, 992px and up) */
+ @media (min-width: $screen-md-min) { width: 170px; }
+
+ /* Large devices (large desktops, 1200px and up) */
+ @media (min-width: $screen-lg-min) { width: 210px; }
+ }
+ }
+
+ /* Hide on extra small devices (phones) */
+ @media (max-width: $screen-xs-max) {
+ display: none;
+ }
+
+ /* Small devices (tablets, 768px and lower) */
+ @media (max-width: $screen-sm-max) {
+ width: 100%;
+ text-align: left;
+
+ input {
+ width: 300px;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index 2cd30491bf5..b6f21fd8c91 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -1,35 +1,11 @@
.gl-pagination {
+ text-align: center;
border-top: 1px solid $border-color;
- background-color: $background-color;
- margin: -$gl-padding;
+ margin: 0;
margin-top: 0;
.pagination {
padding: 0;
- margin: 0;
- display: block;
-
- li.first,
- li.last,
- li.next,
- li.prev {
- > a {
- color: $link-color;
-
- &:hover {
- color: #fff;
- }
- }
- }
-
- li > a,
- li > span {
- border: none;
- margin: 0;
- @include border-radius(0 !important);
- padding: 13px 19px;
- border-right: 1px solid $border-color;
- }
}
}
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index 57b9451b264..ae7bdf14c40 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -2,7 +2,13 @@
margin-bottom: $gl-padding;
.panel-heading {
- padding: 7px $gl-padding;
+ padding: $gl-vert-padding $gl-padding;
+ line-height: 36px;
+
+ .controls {
+ margin-top: -2px;
+ float: right;
+ }
}
.panel-body {
@@ -14,7 +20,3 @@
}
}
}
-
-.container-blank .panel .panel-heading {
- line-height: 42px !important;
-}
diff --git a/app/assets/stylesheets/framework/progress.scss b/app/assets/stylesheets/framework/progress.scss
new file mode 100644
index 00000000000..e9800bd24b5
--- /dev/null
+++ b/app/assets/stylesheets/framework/progress.scss
@@ -0,0 +1,5 @@
+html.turbolinks-progress-bar::before {
+ background-color: $progress-color!important;
+ height: 2px!important;
+ box-shadow: 0 0 10px $progress-color, 0 0 5px $progress-color;
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index af145191bc8..b3371229d5a 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -1,49 +1,53 @@
/** Select2 selectbox style override **/
+.select2-container {
+ width: 100% !important;
+}
+
.select2-container, .select2-container.select2-drop-above {
.select2-choice {
- background: #FFF;
- border-color: #DDD;
- height: 42px;
- padding: 8px $gl-padding;
+ background: #fff;
+ border-color: $input-border;
+ border-color: $border-white-light;
+ height: 35px;
+ padding: $gl-vert-padding $gl-btn-padding;
font-size: $gl-font-size;
line-height: 1.42857143;
- @include border-radius(2px);
+ @include border-radius($border-radius-default);
.select2-arrow {
- background: #FFF;
- border-left: none;
- padding-top: 5px;
+ background-image: none;
+ background-color: transparent;
+ border: none;
+ padding-top: 6px;
+ padding-right: 10px;
+
+ b {
+ @extend .caret;
+ color: $gray-darkest;
+ }
}
.select2-chosen {
- color: $gl-text-color;
+ margin-right: 15px;
}
- &.select2-default {
- .select2-chosen {
- color: #999;
- }
+ &:hover {
+ background-color: $gray-dark;
+ border-color: $border-white-normal;
+ color: $gl-text-color;
}
}
}
-.select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice{
- color: #7f8fa4;
- border: 1px solid #e7e9ed;
-}
-
-
.select2-drop {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
- @include border-radius (0px);
-
- padding: 16px;
- border: none !important;
+ @include border-radius ($border-radius-default);
+ border: none;
}
.select2-results .select2-result-label {
- padding: 9px;
+ padding: 10px 15px;
}
.select2-drop{
@@ -56,15 +60,30 @@
.select2-results li.select2-result-with-children > .select2-result-label {
font-weight: 600;
- color: #313236;
+ color: $gl-text-color;
+}
+
+.select2-container-active {
+ .select2-choice, .select2-choices {
+ @include box-shadow(none);
+ }
+}
+
+.select2-dropdown-open {
+ .select2-choice {
+ border-color: $border-white-normal;
+ outline: 0;
+ background-image: none;
+ background-color: $white-dark;
+ @include box-shadow($gl-btn-active-gradient);
+ }
}
.select2-container-multi {
.select2-choices {
- @include border-radius(2px);
+ @include border-radius($border-radius-default);
border-color: $input-border;
- background: white;
- padding-left: $gl-padding / 2;
+ background: none;
.select2-search-field input {
padding: $gl-padding / 2;
@@ -76,14 +95,16 @@
.select2-search-choice {
margin: 8px 0 0 8px;
- background: white;
box-shadow: none;
border-color: $input-border;
color: $gl-text-color;
line-height: 15px;
+ background-color: $background-color;
+ background-image: none;
.select2-search-choice-close {
- top: 5px;
+ top: 4px;
+ left: 3px;
}
&.select2-search-choice-focus {
@@ -91,22 +112,25 @@
}
}
}
+
+ &.select2-container-active .select2-choices,
+ &.select2-dropdown-open .select2-choices {
+ border-color: $border-white-normal;
+ @include box-shadow($gl-btn-active-gradient);
+ }
+}
+
+.select2-container-multi .select2-choices .select2-search-choice {
}
.select2-drop-active {
- border: 1px solid #BBB !important;
- margin-top: 4px;
- font-size: 13px;
+ margin-top: 6px;
+ font-size: 14px;
&.select2-drop-above {
margin-bottom: 8px;
}
- .select2-search input {
- background: #fafafa;
- border-color: #DDD;
- }
-
.select2-results {
max-height: 350px;
.select2-highlighted {
@@ -115,8 +139,34 @@
}
}
-.select2-container {
- width: 100% !important;
+.select2-search {
+ padding: 15px 15px 5px;
+
+ .select2-drop-auto-width & {
+ padding: 15px 15px 5px;
+ }
+}
+
+.select2-search input {
+ padding: 2px 25px 2px 5px;
+ background: #fff image-url('select2.png');
+ background-repeat: no-repeat;
+ background-position: right 0 bottom 6px;
+ border: 1px solid $input-border;
+ @include border-radius($border-radius-default);
+ @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s);
+
+ &:focus {
+ border-color: $input-border-focus;
+ }
+}
+
+.select2-search input.select2-active {
+ background-color: #fff;
+ background-image: image-url('select2-spinner.gif') !important;
+ background-repeat: no-repeat;
+ background-position: right 5px center !important;
+ background-size: 16px 16px !important;
}
/** Branch/tag selector **/
@@ -124,10 +174,19 @@
width: 160px !important;
}
-.ajax-users-dropdown, .ajax-project-users-dropdown {
- .select2-search {
- padding-top: 2px;
- }
+.select2-results .select2-no-results,
+.select2-results .select2-searching,
+.select2-results .select2-ajax-error,
+.select2-results .select2-selection-limit {
+ background: $gray-light;
+ display: list-item;
+ padding: 10px 15px;
+}
+
+
+.select2-results {
+ margin: 0;
+ padding: 10px 0;
}
.ajax-users-select {
@@ -170,7 +229,7 @@
.namespace-result {
.namespace-kind {
- color: #AAA;
+ color: #aaa;
font-weight: normal;
}
.namespace-path {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 458af76cb75..be05db58c40 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -12,20 +12,43 @@
height: 100%;
transition-duration: .3s;
}
+
+ .gitlab-text-container-link {
+ z-index: 1;
+ position: absolute;
+ left: 0;
+ }
+
+ #logo {
+ z-index: 2;
+ position: absolute;
+ width: 58px;
+ cursor: pointer;
+ }
+
+ &.right-sidebar-expanded {
+ /* Extra small devices (phones, less than 768px) */
+ /* No media query since this is the default in Bootstrap */
+ padding-right: 0;
+ /* Small devices (tablets, 768px and up) */
+ @media (min-width: $screen-sm-min) {
+ padding-right: $gutter_width;
+ }
+
+ }
}
.sidebar-wrapper {
- z-index: 99;
+ z-index: 999;
background: $background-color;
}
.content-wrapper {
width: 100%;
- padding: 20px;
.container-fluid {
- background: #FFF;
- padding: $gl-padding;
+ background: #fff;
+ padding: 0 $gl-padding;
&.container-blank {
background: none;
@@ -71,7 +94,7 @@
width: 158px;
float: left;
margin: 0;
- margin-left: 14px;
+ margin-left: 50px;
font-size: 19px;
line-height: 41px;
font-weight: normal;
@@ -80,7 +103,7 @@
}
&:hover {
- background-color: #EEE;
+ background-color: #eee;
}
}
@@ -105,7 +128,7 @@
.tanuki-shape {
transition: all 0.8s;
- &:hover {
+ &:hover, &.highlight {
fill: rgb(255, 255, 255);
transition: all 0.1s;
}
@@ -120,7 +143,7 @@
overflow: hidden;
&.navbar-collapse {
- padding: 0px !important;
+ padding: 0 !important;
}
li {
@@ -159,7 +182,7 @@
.count {
float: right;
background: #eee;
- padding: 0px 8px;
+ padding: 0 8px;
@include border-radius(6px);
}
@@ -171,8 +194,8 @@
}
.sidebar-subnav {
- margin-left: 0px;
- padding-left: 0px;
+ margin-left: 0;
+ padding-left: 0;
li {
list-style: none;
@@ -180,7 +203,20 @@
}
@mixin expanded-sidebar {
- padding-left: $sidebar_width;
+ padding-left: $sidebar_collapsed_width;
+
+ @media (min-width: $screen-md-min) {
+ padding-left: $sidebar_width;
+ }
+
+ &.right-sidebar-collapsed {
+ /* Extra small devices (phones, less than 768px) */
+ padding-right: 0;
+ /* Small devices (tablets, 768px and up) */
+ @media (min-width: $screen-sm-min) {
+ padding-right: $sidebar_collapsed_width;
+ }
+ }
.sidebar-wrapper {
width: $sidebar_width;
@@ -204,6 +240,15 @@
@mixin collapsed-sidebar {
padding-left: $sidebar_collapsed_width;
+ &.right-sidebar-collapsed {
+ /* Extra small devices (phones, less than 768px) */
+ padding-right: 0;
+ /* Small devices (tablets, 768px and up) */
+ @media (min-width: $screen-sm-min) {
+ padding-right: $sidebar_collapsed_width;
+ }
+ }
+
.sidebar-wrapper {
width: $sidebar_collapsed_width;
@@ -267,26 +312,16 @@
background: #f2f6f7;
}
-@media (max-width: $screen-md-max) {
- .page-sidebar-collapsed {
- @include collapsed-sidebar;
- }
-
- .page-sidebar-expanded {
+.page-sidebar-collapsed {
+ /* Extra small devices (phones, less than 768px) */
+ @include collapsed-sidebar;
+ padding-right: 0;
+ /* Small devices (tablets, 768px and up) */
+ @media (min-width: $screen-sm-min) {
@include collapsed-sidebar;
}
-
- .collapse-nav {
- display: none;
- }
}
-@media(min-width: $screen-md-max) {
- .page-sidebar-collapsed {
- @include collapsed-sidebar;
- }
-
- .page-sidebar-expanded {
- @include expanded-sidebar;
- }
+.page-sidebar-expanded {
+ @include expanded-sidebar;
}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 793ab3d9bb9..75b770ae5a2 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -1,13 +1,11 @@
.table-holder {
- margin: -$gl-padding;
- margin-top: 0;
- margin-bottom: 0;
+ margin: 0;
}
table {
&.table {
margin-bottom: $gl-padding;
-
+
.dropdown-menu a {
text-decoration: none;
}
@@ -32,14 +30,15 @@ table {
}
th {
+ background-color: $background-color;
font-weight: normal;
font-size: 15px;
- border-bottom: 1px solid $border-color !important;
+ border-bottom: 1px solid $border-color;
}
td {
- border-color: $table-border-color !important;
- border-bottom: 1px solid;
+ border-color: $table-border-color;
+ border-bottom: 1px solid $border-color;
}
}
}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index ff41e26ed8a..aa244fe548d 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -5,15 +5,13 @@
padding: 0;
.timeline-entry {
- padding: $gl-padding;
+ padding: $gl-padding $gl-btn-padding;
border-color: $table-border-color;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
color: $gl-gray;
border-bottom: 1px solid $border-white-light;
&:target {
- background: $hover;
+ background: $row-hover;
}
&:last-child {
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 94f0ed761df..dd42db1840f 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -22,7 +22,7 @@
// Components
@import "bootstrap/component-animations";
-@import "bootstrap/dropdowns";
+// @import "bootstrap/dropdowns";
@import "bootstrap/button-groups";
@import "bootstrap/input-groups";
@import "bootstrap/navs";
@@ -95,51 +95,10 @@
}
&.label-inverse {
- background-color: #333333;
+ background-color: #333;
}
}
-// Nav tabs
-.nav.nav-tabs {
- margin-bottom: 15px;
-
- li {
- > a {
- margin-right: 5px;
- line-height: 20px;
- border-color: #EEE;
- color: #888;
- border-bottom: 1px solid #ddd;
- .badge {
- background-color: #eee;
- color: #888;
- text-shadow: 0 1px 1px #fff;
- }
- i.fa {
- line-height: 14px;
- }
- }
- &.active {
- > a {
- border-color: #CCC;
- border-bottom: 1px solid #fff;
- color: #333;
- font-weight: bold;
- }
- }
- }
-}
-
-.nav-tabs > li > a,
-.nav-pills > li > a {
- color: #666;
-}
-
-.nav-pills > .active > a > span > .badge {
- background-color: #fff;
- color: $gl-primary;
-}
-
/**
* fix to keep tooltips position in top navigation bar
@@ -155,22 +114,9 @@
*
*/
-.container-blank .panel .panel-heading {
- font-size: 17px;
- line-height: 38px;
-}
-
.panel {
box-shadow: none;
- .panel-heading {
- .panel-head-actions {
- position: relative;
- top: -5px;
- float: right;
- }
- }
-
.panel-body {
form, pre {
margin: 0;
@@ -192,7 +138,7 @@
}
.btn-clipboard {
- min-width: 0px;
+ min-width: 0;
}
}
@@ -221,12 +167,6 @@
}
}
-.alert-help {
- background-color: $background-color;
- border: 1px solid $border-color;
- color: $gl-gray;
-}
-
// Typography =================================================================
.text-primary,
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 63868a34e2a..f63ac033234 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -22,9 +22,9 @@ $brand-info: $gl-info;
$brand-warning: $gl-warning;
$brand-danger: $gl-danger;
-$border-radius-base: 2px !default;
-$border-radius-large: 2px !default;
-$border-radius-small: 2px !default;
+$border-radius-base: 3px !default;
+$border-radius-large: 3px !default;
+$border-radius-small: 3px !default;
//== Scaffolding
@@ -46,7 +46,7 @@ $font-size-base: $gl-font-size;
//
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-$padding-base-vertical: 9px;
+$padding-base-vertical: $gl-vert-padding;
$padding-base-horizontal: $gl-padding;
$component-active-color: #fff;
$component-active-bg: $brand-info;
@@ -57,7 +57,7 @@ $component-active-bg: $brand-info;
$input-color: $text-color;
$input-border: #e7e9ed;
-$input-border-focus: #7F8FA4;
+$input-border-focus: #7f8fa4;
$legend-color: $text-color;
@@ -66,20 +66,20 @@ $legend-color: $text-color;
//##
$pagination-color: $gl-gray;
-$pagination-bg: $background-color;
-$pagination-border: transparent;
+$pagination-bg: #fff;
+$pagination-border: $border-color;
-$pagination-hover-color: #fff;
-$pagination-hover-bg: $brand-info;
-$pagination-hover-border: transparent;
+$pagination-hover-color: $gl-gray;
+$pagination-hover-bg: $row-hover;
+$pagination-hover-border: $border-color;
-$pagination-active-color: #fff;
-$pagination-active-bg: $brand-info;
-$pagination-active-border: transparent;
+$pagination-active-color: $blue-dark;
+$pagination-active-bg: #fff;
+$pagination-active-border: $border-color;
-$pagination-disabled-color: #fff;
-$pagination-disabled-bg: lighten($brand-info, 15%);
-$pagination-disabled-border: transparent;
+$pagination-disabled-color: #cdcdcd;
+$pagination-disabled-bg: $background-color;
+$pagination-disabled-border: $border-color;
//== Form states and alerts
@@ -125,8 +125,8 @@ $panel-inner-border: $border-color;
//
//##
-$well-bg: #F9F9F9;
-$well-border: #EEE;
+$well-bg: #f9f9f9;
+$well-border: #eee;
//== Code
//
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index c3e4ad0ad00..949295a1d0c 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -27,13 +27,13 @@
line-height: 10px;
color: #555;
vertical-align: middle;
- background-color: #FCFCFC;
+ background-color: #fcfcfc;
border-width: 1px;
border-style: solid;
- border-color: #CCC #CCC #BBB;
+ border-color: #ccc #ccc #bbb;
border-image: none;
border-radius: 3px;
- box-shadow: 0px -1px 0px #BBB inset;
+ box-shadow: 0 -1px 0 #bbb inset;
}
h1 {
@@ -54,17 +54,17 @@
h3 {
margin: 24px 0 12px 0;
- font-size: 1.25em;
+ font-size: 1.1em;
}
h4 {
margin: 24px 0 12px 0;
- font-size: 1.1em;
+ font-size: 0.98em;
}
h5 {
margin: 24px 0 12px 0;
- font-size: 1em;
+ font-size: 0.95em;
}
h6 {
@@ -87,8 +87,8 @@
}
p {
- color:#5c5d5e;
- margin:6px 0 0 0;
+ color: #5c5d5e;
+ margin: 6px 0 0 0;
}
table {
@@ -102,11 +102,10 @@
}
pre {
- margin: 12px 0 12px 0 !important;
- background-color: #f8fafc;
- font-size: 13px !important;
- color: #5b6169;
- line-height: 1.6em !important;
+ margin: 12px 0 12px 0;
+ font-size: 13px;
+ line-height: 1.6em;
+ overflow-x: auto;
@include border-radius(2px);
}
@@ -116,7 +115,7 @@
ul, ol {
padding: 0;
- margin: 6px 0 6px 18px !important;
+ margin: 6px 0 6px 28px !important;
}
li {
@@ -150,13 +149,13 @@
}
&:hover > a.anchor {
- $size: 16px;
+ $size: 14px;
position: absolute;
right: 100%;
top: 50%;
- margin-top: -$size/2;
- margin-right: 0px;
- padding-right: 20px;
+ margin-top: -11px;
+ margin-right: 0;
+ padding-right: 15px;
display: inline-block;
width: $size;
height: $size;
@@ -177,7 +176,7 @@ body {
}
.page-title {
- margin-top: 0px;
+ margin-top: $gl-padding;
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
@@ -188,7 +187,7 @@ body {
}
.page-title-empty {
- margin-top: 0px;
+ margin-top: 0;
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
@@ -197,18 +196,13 @@ body {
h1, h2, h3, h4, h5, h6 {
color: $gl-header-color;
- font-weight: 500;
+ font-weight: 600;
}
/** CODE **/
pre {
font-family: $monospace_font;
- &.dark {
- background: #333;
- color: $background-color;
- }
-
&.plain-readme {
background: none;
border: none;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index af75123b0af..211ead7319d 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,17 +1,20 @@
-$hover: #faf9f9;
-$gl-text-color: #54565B;
-$gl-text-green: #4A2;
-$gl-text-red: #D12F19;
-$gl-text-orange: #D90;
+$row-hover: #f4f8fe;
+$gl-text-color: #54565b;
+$gl-text-green: #4a2;
+$gl-text-red: #d12f19;
+$gl-text-orange: #d90;
$gl-header-color: #323232;
$gl-link-color: #333c48;
$md-text-color: #444;
$md-link-color: #3084bb;
-$nprogress-color: #c0392b;
+$progress-color: #c0392b;
$gl-font-size: 15px;
$list-font-size: 15px;
$sidebar_collapsed_width: 62px;
$sidebar_width: 230px;
+$gutter_collapsed_width: 62px;
+$gutter_width: 290px;
+$gutter_inner_width: 258px;
$avatar_radius: 50%;
$code_font_size: 13px;
$code_line_height: 1.5;
@@ -22,64 +25,87 @@ $header-height: 58px;
$fixed-layout-width: 1280px;
$gl-gray: #5a5a5a;
$gl-padding: 16px;
-$gl-padding-top:10px;
-$gl-avatar-size: 46px;
+$gl-btn-padding: 10px;
+$gl-vert-padding: 6px;
+$gl-padding-top: 10px;
+$gl-avatar-size: 40px;
+$secondary-text: #7f8fa4;
+$error-exclamation-point: #e62958;
+$border-radius-default: 3px;
+$list-title-color: #333;
+$list-text-color: #555;
+
+$btn-transparent-color: #8f8f8f;
+
+$ssh-key-icon-color: #8f8f8f;
+$ssh-key-icon-size: 18px;
+
+$provider-btn-group-border: #e5e5e5;
+$provider-btn-not-active-color: #4688f1;
/*
* Color schema
*/
-$white-light: #FFFFFF;
+$white-light: #fff;
$white-normal: #ededed;
$white-dark: #ededed;
-$gray-light: #f7f7f7;
-$gray-normal: #ededed;
+$gray-light: #faf9f9;
+$gray-normal: #f5f5f5;
$gray-dark: #ededed;
+$gray-darkest: #c9c9c9;
-$green-light: #31AF64;
-$green-normal: #2FAA60;
-$green-dark: #2CA05B;
+$green-light: #38ae67;
+$green-normal: #2faa60;
+$green-dark: #2ca05b;
-$blue-light: #2EA8E5;
-$blue-normal: #2D9FD8;
-$blue-dark: #2897CE;
+$blue-light: #2ea8e5;
+$blue-normal: #2d9fd8;
+$blue-dark: #2897ce;
-$blue-medium-light: #3498CB;
-$blue-medium: #2F8EBF;
-$blue-medium-dark: #2D86B4;
+$blue-medium-light: #3498cb;
+$blue-medium: #2f8ebf;
+$blue-medium-dark: #2d86b4;
-$orange-light: #FC6443;
-$orange-normal: #E75E40;
-$orange-dark: #CE5237;
+$orange-light: rgba(252, 109, 38, 0.80);
+$orange-normal: #e75e40;
+$orange-dark: #ce5237;
-$red-light: #F43263;
-$red-normal: #E52C5A;
-$red-dark: #D22852;
+$red-light: #f06559;
+$red-normal: #e52c5a;
+$red-dark: #d22852;
-$border-white-light: #F1F2F4;
-$border-white-normal: #D6DAE2;
-$border-white-dark: #C6CACF;
+$border-white-light: #f1f2f4;
+$border-white-normal: #d6dae2;
+$border-white-dark: #c6cacf;
-$border-gray-light: #d1d1d1;
-$border-gray-normal: #D6DAE2;
-$border-gray-dark: #C6CACF;
+$border-gray-light: rgba(0, 0, 0, 0.06);
+$border-gray-normal: rgba(0, 0, 0, 0.10);;
+$border-gray-dark: #c6cacf;
-$border-green-light: #2FAA60;
-$border-green-normal: #2CA05B;
+$border-green-light: #2faa60;
+$border-green-normal: #2ca05b;
$border-green-dark: #279654;
-$border-blue-light: #2D9FD8;
-$border-blue-normal: #2897CE;
-$border-blue-dark: #258DC1;
+$border-blue-light: #2d9fd8;
+$border-blue-normal: #2897ce;
+$border-blue-dark: #258dc1;
+
+$border-orange-light: #fc6d26;
+$border-orange-normal: #ce5237;
+$border-orange-dark: #c14e35;
+
+$border-red-light: #f24f41;
+$border-red-normal: #d22852;
+$border-red-dark: #ca264f;
-$border-orange-light: #ED5C3D;
-$border-orange-normal: #CE5237;
-$border-orange-dark: #C14E35;
+$help-well-bg: #fafafa;
+$help-well-border: #e5e5e5;
-$border-red-light: #E52C5A;
-$border-red-normal: #D22852;
-$border-red-dark: #CA264F;
+$warning-message-bg: #fbf2d9;
+$warning-message-color: #9e8e60;
+$warning-message-border: #f0e2bb;
/* header */
$light-grey-header: #faf9f9;
@@ -92,6 +118,8 @@ $gl-success: $green-normal;
$gl-info: $blue-normal;
$gl-warning: $orange-normal;
$gl-danger: $red-normal;
+$gl-btn-active-background: rgba(0, 0, 0, 0.12);
+$gl-btn-active-gradient: inset 0 0 4px $gl-btn-active-background;
/*
* Commit Diff Colors
@@ -104,3 +132,33 @@ $deleted: #f77;
*/
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif;
+
+/*
+* Dropdowns
+*/
+$dropdown-bg: #fff;
+$dropdown-link-color: #555;
+$dropdown-link-hover-bg: rgba(#000, .04);
+$dropdown-border-color: rgba(#000, .1);
+$dropdown-shadow-color: rgba(#000, .1);
+$dropdown-divider-color: rgba(#000, .1);
+$dropdown-header-color: #959494;
+$dropdown-title-btn-color: #bfbfbf;
+$dropdown-input-color: #c7c7c7;
+$dropdown-input-focus-border: rgb(58, 171, 240);
+$dropdown-input-focus-shadow: rgba(#000, .2);
+$dropdown-loading-bg: rgba(#fff, .6);
+
+$dropdown-toggle-bg: #fff;
+$dropdown-toggle-color: #626262;
+$dropdown-toggle-border-color: #eaeaea;
+$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
+$dropdown-toggle-icon-color: #c4c4c4;
+$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
+
+/*
+ * Award emoji
+ */
+$award-emoji-menu-bg: #fff;
+$award-emoji-menu-border: #f1f2f4;
+$award-emoji-new-btn-icon-color: #dcdcdc;
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 32e2c020e06..02e24ec7c4d 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -1,17 +1,13 @@
.zennable {
- .zen-toggle-comment {
- display: none;
- }
-
- .zen-enter-link {
+ a.js-zen-enter {
color: $gl-gray;
position: absolute;
- top: 0px;
+ top: 0;
right: 4px;
- line-height: 40px;
+ line-height: 56px;
}
- .zen-leave-link {
+ a.js-zen-leave {
display: none;
color: $gl-text-color;
position: absolute;
@@ -25,62 +21,41 @@
}
}
- // Hide the Enter link when we're in Zen mode
- input:checked ~ .zen-backdrop .zen-enter-link {
- display: none;
- }
-
- // Show the Leave link when we're in Zen mode
- input:checked ~ .zen-backdrop .zen-leave-link {
- display: block;
- position: absolute;
- top: 0;
- }
-
- input:checked ~ .zen-backdrop {
- background-color: white;
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 1031;
-
- textarea {
- border: none;
- box-shadow: none;
- border-radius: 0;
- color: #000;
- font-size: 20px;
- line-height: 26px;
- padding: 30px;
- display: block;
- outline: none;
- resize: none;
- height: 100vh;
- max-width: 900px;
- margin: 0 auto;
+ .zen-backdrop {
+ &.fullscreen {
+ background-color: white;
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 1031;
+
+ textarea {
+ border: none;
+ box-shadow: none;
+ border-radius: 0;
+ color: #000;
+ font-size: 20px;
+ line-height: 26px;
+ padding: 30px;
+ display: block;
+ outline: none;
+ resize: none;
+ height: 100vh;
+ max-width: 900px;
+ margin: 0 auto;
+ }
+
+ a.js-zen-enter {
+ display: none;
+ }
+
+ a.js-zen-leave {
+ display: block;
+ position: absolute;
+ top: 0;
+ }
}
}
-
- // Make the color of the placeholder text in the Zenned-out textarea darker,
- // so it becomes visible
-
- input:checked ~ .zen-backdrop textarea::-webkit-input-placeholder {
- color: #A8A8A8;
- }
-
- input:checked ~ .zen-backdrop textarea:-moz-placeholder {
- color: #A8A8A8;
- opacity: 1;
- }
-
- input:checked ~ .zen-backdrop textarea::-moz-placeholder {
- color: #A8A8A8;
- opacity: 1;
- }
-
- input:checked ~ .zen-backdrop textarea:-ms-input-placeholder {
- color: #A8A8A8;
- }
}
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 6a2b25ddc67..47673944896 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -1,18 +1,38 @@
/* https://github.com/MozMorris/tomorrow-pygments */
.code.dark {
+ // Line numbers
+ .line-numbers, .diff-line-num {
+ background-color: #1d1f21;
+ }
+
+ .diff-line-num, .diff-line-num a {
+ color: rgba(255, 255, 255, 0.3);
+ }
- background-color: #1d1f21 !important;
- color: #c5c8c6 !important;
+ // Code itself
+ pre.code, .diff-line-num {
+ border-color: #666;
+ }
- pre.highlight,
- .line-numbers,
- .line-numbers a {
- background-color: #1d1f21 !important;
- color: #c5c8c6 !important;
+ &, pre.code, .line_holder .line_content {
+ background-color: #1d1f21;
+ color: #c5c8c6;
}
- pre.code {
- border-left: 1px solid #666;
+ // Diff line
+ .line_holder {
+ .diff-line-num.new, .line_content.new {
+ @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080);
+ }
+
+ .diff-line-num.old, .line_content.old {
+ @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.25), #808080);
+ }
+
+ .line_content.match {
+ color: rgba(255, 255, 255, 0.3);
+ background: rgba(255, 255, 255, 0.1);
+ }
}
// highlight line via anchor
@@ -23,12 +43,12 @@
// Search result highlight
span.highlight_word {
background-color: #ffe792 !important;
- color: #000000 !important;
+ color: #000 !important;
}
.hll { background-color: #373b41 }
.c { color: #969896 } /* Comment */
- .err { color: #cc6666 } /* Error */
+ .err { color: #c66 } /* Error */
.k { color: #b294bb } /* Keyword */
.l { color: #de935f } /* Literal */
.n { color: #c5c8c6 } /* Name */
@@ -38,7 +58,7 @@
.cp { color: #969896 } /* Comment.Preproc */
.c1 { color: #969896 } /* Comment.Single */
.cs { color: #969896 } /* Comment.Special */
- .gd { color: #cc6666 } /* Generic.Deleted */
+ .gd { color: #c66 } /* Generic.Deleted */
.ge { font-style: italic } /* Generic.Emph */
.gh { color: #c5c8c6; font-weight: bold } /* Generic.Heading */
.gi { color: #b5bd68 } /* Generic.Inserted */
@@ -57,17 +77,17 @@
.na { color: #81a2be } /* Name.Attribute */
.nb { color: #c5c8c6 } /* Name.Builtin */
.nc { color: #f0c674 } /* Name.Class */
- .no { color: #cc6666 } /* Name.Constant */
+ .no { color: #c66 } /* Name.Constant */
.nd { color: #8abeb7 } /* Name.Decorator */
.ni { color: #c5c8c6 } /* Name.Entity */
- .ne { color: #cc6666 } /* Name.Exception */
+ .ne { color: #c66 } /* Name.Exception */
.nf { color: #81a2be } /* Name.Function */
.nl { color: #c5c8c6 } /* Name.Label */
.nn { color: #f0c674 } /* Name.Namespace */
.nx { color: #81a2be } /* Name.Other */
.py { color: #c5c8c6 } /* Name.Property */
.nt { color: #8abeb7 } /* Name.Tag */
- .nv { color: #cc6666 } /* Name.Variable */
+ .nv { color: #c66 } /* Name.Variable */
.ow { color: #8abeb7 } /* Operator.Word */
.w { color: #c5c8c6 } /* Text.Whitespace */
.mf { color: #de935f } /* Literal.Number.Float */
@@ -86,8 +106,8 @@
.s1 { color: #b5bd68 } /* Literal.String.Single */
.ss { color: #b5bd68 } /* Literal.String.Symbol */
.bp { color: #c5c8c6 } /* Name.Builtin.Pseudo */
- .vc { color: #cc6666 } /* Name.Variable.Class */
- .vg { color: #cc6666 } /* Name.Variable.Global */
- .vi { color: #cc6666 } /* Name.Variable.Instance */
+ .vc { color: #c66 } /* Name.Variable.Class */
+ .vg { color: #c66 } /* Name.Variable.Global */
+ .vi { color: #c66 } /* Name.Variable.Instance */
.il { color: #de935f } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 8560c3c490f..806401c21ae 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -1,18 +1,38 @@
/* https://github.com/richleland/pygments-css/blob/master/monokai.css */
.code.monokai {
+ // Line numbers
+ .line-numbers, .diff-line-num {
+ background-color: #272822;
+ }
+
+ .diff-line-num, .diff-line-num a {
+ color: rgba(255, 255, 255, 0.3);
+ }
- background-color: #272822 !important;
- color: #f8f8f2 !important;
+ // Code itself
+ pre.code, .diff-line-num {
+ border-color: #555;
+ }
- pre.highlight,
- .line-numbers,
- .line-numbers a {
- background-color :#272822 !important;
- color: #f8f8f2 !important;
+ &, pre.code, .line_holder .line_content {
+ background-color: #272822;
+ color: #f8f8f2;
}
- pre.code {
- border-left: 1px solid #555;
+ // Diff line
+ .line_holder {
+ .diff-line-num.new, .line_content.new {
+ @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080);
+ }
+
+ .diff-line-num.old, .line_content.old {
+ @include diff_background(rgba(254, 147, 140, 0.15), rgba(254, 147, 140, 0.2), #808080);
+ }
+
+ .line_content.match {
+ color: rgba(255, 255, 255, 0.3);
+ background: rgba(255, 255, 255, 0.1);
+ }
}
// highlight line via anchor
@@ -23,7 +43,7 @@
// Search result highlight
span.highlight_word {
background-color: #ffe792 !important;
- color: #000000 !important;
+ color: #000 !important;
}
.hll { background-color: #49483e }
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 7d489a9666b..6a809d4dfd2 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -1,18 +1,38 @@
/* https://gist.github.com/qguv/7936275 */
.code.solarized-dark {
+ // Line numbers
+ .line-numbers, .diff-line-num {
+ background-color: #002b36;
+ }
+
+ .diff-line-num, .diff-line-num a {
+ color: rgba(255, 255, 255, 0.3);
+ }
- background-color: #002b36 !important;
- color: #93a1a1 !important;
+ // Code itself
+ pre.code, .diff-line-num {
+ border-color: #113b46;
+ }
- pre.highlight,
- .line-numbers,
- .line-numbers a {
- background-color: #002b36 !important;
- color: #93a1a1 !important;
+ &, pre.code, .line_holder .line_content {
+ background-color: #002b36;
+ color: #93a1a1;
}
- pre.code {
- border-left: 1px solid #113b46;
+ // Diff line
+ .line_holder {
+ .diff-line-num.new, .line_content.new {
+ @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46);
+ }
+
+ .diff-line-num.old, .line_content.old {
+ @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.25), #113b46);
+ }
+
+ .line_content.match {
+ color: rgba(255, 255, 255, 0.3);
+ background: rgba(255, 255, 255, 0.1);
+ }
}
// highlight line via anchor
@@ -76,7 +96,7 @@
.m { color: #2aa198 } /* Literal.Number */
.s { color: #2aa198 } /* Literal.String */
.na { color: #93a1a1 } /* Name.Attribute */
- .nb { color: #B58900 } /* Name.Builtin */
+ .nb { color: #b58900 } /* Name.Builtin */
.nc { color: #268bd2 } /* Name.Class */
.no { color: #cb4b16 } /* Name.Constant */
.nd { color: #268bd2 } /* Name.Decorator */
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 200ed346446..b90c95c62d1 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -1,18 +1,38 @@
/* https://gist.github.com/qguv/7936275 */
.code.solarized-light {
+ // Line numbers
+ .line-numbers, .diff-line-num {
+ background-color: #fdf6e3;
+ }
+
+ .diff-line-num, .diff-line-num a {
+ color: rgba(0, 0, 0, 0.3);
+ }
- background-color: #fdf6e3 !important;
- color: #586e75 !important;
+ // Code itself
+ pre.code, .diff-line-num {
+ border-color: #c5d0d4;
+ }
- pre.highlight,
- .line-numbers,
- .line-numbers a {
- background-color: #fdf6e3 !important;
- color: #586e75 !important;
+ &, pre.code, .line_holder .line_content {
+ background-color: #fdf6e3;
+ color: #586e75;
}
- pre.code {
- border-left: 1px solid #c5d0d4;
+ // Diff line
+ .line_holder {
+ .diff-line-num.new, .line_content.new {
+ @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4);
+ }
+
+ .diff-line-num.old, .line_content.old {
+ @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.25), #c5d0d4);
+ }
+
+ .line_content.match {
+ color: rgba(0, 0, 0, 0.3);
+ background: rgba(255, 255, 255, 0.4);
+ }
}
// highlight line via anchor
@@ -76,7 +96,7 @@
.m { color: #2aa198 } /* Literal.Number */
.s { color: #2aa198 } /* Literal.String */
.na { color: #586e75 } /* Name.Attribute */
- .nb { color: #B58900 } /* Name.Builtin */
+ .nb { color: #b58900 } /* Name.Builtin */
.nc { color: #268bd2 } /* Name.Class */
.no { color: #cb4b16 } /* Name.Constant */
.nd { color: #268bd2 } /* Name.Decorator */
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index e2626da7871..8c1b0cd84ec 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,20 +1,60 @@
/* https://github.com/aahan/pygments-github-style */
.code.white {
+ // Line numbers
+ .line-numbers, .diff-line-num {
+ background-color: $background-color;
+ }
- background-color: #f8fafc !important;
- color: #5b6169 !important;
+ .diff-line-num, .diff-line-num a {
+ color: rgba(0, 0, 0, 0.3);
+ }
- pre.highlight,
- .line-numbers,
- .line-numbers a {
- background-color: $background-color !important;
- color: $gl-gray !important;
+ // Code itself
+ pre.code, .diff-line-num {
+ border-color: $border-color;
}
- pre.code {
- border-left: 1px solid $border-color;
- background-color: #fff !important;
- color: #333 !important;
+ &, pre.code, .line_holder .line_content {
+ background-color: #fff;
+ color: #333;
+ }
+
+ // Diff line
+ .line_holder {
+ .diff-line-num {
+ &.old {
+ background: #fdd;
+ border-color: #f1c0c0;
+ }
+
+ &.new {
+ background: #dbffdb;
+ border-color: #c1e9c1;
+ }
+ }
+
+ .line_content {
+ &.old {
+ background: #ffecec;
+
+ span.idiff {
+ background-color: #f8cbcb;
+ }
+ }
+
+ &.new {
+ background: #eaffea;
+
+ span.idiff {
+ background-color: #a6f3a6;
+ }
+ }
+
+ &.match {
+ color: rgba(0, 0, 0, 0.3);
+ background: #fafafa;
+ }
+ }
}
// highlight line via anchor
@@ -28,66 +68,66 @@
}
.hll { background-color: #f8f8f8 }
- .c { color: #999988; font-style: italic; }
+ .c { color: #998; font-style: italic; }
.err { color: #a61717; background-color: #e3d2d2; }
.k { font-weight: bold; }
.o { font-weight: bold; }
- .cm { color: #999988; font-style: italic; }
- .cp { color: #999999; font-weight: bold; }
- .c1 { color: #999988; font-style: italic; }
- .cs { color: #999999; font-weight: bold; font-style: italic; }
- .gd { color: #000000; background-color: #ffdddd; }
- .gd .x { color: #000000; background-color: #ffaaaa; }
+ .cm { color: #998; font-style: italic; }
+ .cp { color: #999; font-weight: bold; }
+ .c1 { color: #998; font-style: italic; }
+ .cs { color: #999; font-weight: bold; font-style: italic; }
+ .gd { color: #000; background-color: #fdd; }
+ .gd .x { color: #000; background-color: #faa; }
.ge { font-style: italic; }
- .gr { color: #aa0000; }
- .gh { color: #999999; }
- .gi { color: #000000; background-color: #ddffdd; }
- .gi .x { color: #000000; background-color: #aaffaa; }
- .go { color: #888888; }
- .gp { color: #555555; }
+ .gr { color: #a00; }
+ .gh { color: #999; }
+ .gi { color: #000; background-color: #dfd; }
+ .gi .x { color: #000; background-color: #afa; }
+ .go { color: #888; }
+ .gp { color: #555; }
.gs { font-weight: bold; }
.gu { color: #800080; font-weight: bold; }
- .gt { color: #aa0000; }
+ .gt { color: #a00; }
.kc { font-weight: bold; }
.kd { font-weight: bold; }
.kn { font-weight: bold; }
.kp { font-weight: bold; }
.kr { font-weight: bold; }
- .kt { color: #445588; font-weight: bold; }
- .m { color: #009999; }
- .s { color: #dd1144; }
- .n { color: #333333; }
+ .kt { color: #458; font-weight: bold; }
+ .m { color: #099; }
+ .s { color: #d14; }
+ .n { color: #333; }
.na { color: teal; }
.nb { color: #0086b3; }
- .nc { color: #445588; font-weight: bold; }
+ .nc { color: #458; font-weight: bold; }
.no { color: teal; }
.ni { color: purple; }
- .ne { color: #990000; font-weight: bold; }
- .nf { color: #990000; font-weight: bold; }
- .nn { color: #555555; }
+ .ne { color: #900; font-weight: bold; }
+ .nf { color: #900; font-weight: bold; }
+ .nn { color: #555; }
.nt { color: navy; }
.nv { color: teal; }
.ow { font-weight: bold; }
- .w { color: #bbbbbb; }
- .mf { color: #009999; }
- .mh { color: #009999; }
- .mi { color: #009999; }
- .mo { color: #009999; }
- .sb { color: #dd1144; }
- .sc { color: #dd1144; }
- .sd { color: #dd1144; }
- .s2 { color: #dd1144; }
- .se { color: #dd1144; }
- .sh { color: #dd1144; }
- .si { color: #dd1144; }
- .sx { color: #dd1144; }
+ .w { color: #bbb; }
+ .mf { color: #099; }
+ .mh { color: #099; }
+ .mi { color: #099; }
+ .mo { color: #099; }
+ .sb { color: #d14; }
+ .sc { color: #d14; }
+ .sd { color: #d14; }
+ .s2 { color: #d14; }
+ .se { color: #d14; }
+ .sh { color: #d14; }
+ .si { color: #d14; }
+ .sx { color: #d14; }
.sr { color: #009926; }
- .s1 { color: #dd1144; }
+ .s1 { color: #d14; }
.ss { color: #990073; }
- .bp { color: #999999; }
+ .bp { color: #999; }
.vc { color: teal; }
.vg { color: teal; }
.vi { color: teal; }
- .il { color: #009999; }
- .gc { color: #999; background-color: #EAF2F5; }
+ .il { color: #099; }
+ .gc { color: #999; background-color: #eaf2f5; }
}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 144852e7874..a61161810a3 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -55,6 +55,16 @@
@extend .alert-warning;
padding: 10px;
text-align: center;
+
+ > div, p {
+ display: inline;
+ margin: 0;
+
+ a {
+ color: inherit;
+ text-decoration: underline;
+ }
+ }
}
.broadcast-message-preview {
diff --git a/app/assets/stylesheets/pages/appearances.scss b/app/assets/stylesheets/pages/appearances.scss
new file mode 100644
index 00000000000..878f44116ba
--- /dev/null
+++ b/app/assets/stylesheets/pages/appearances.scss
@@ -0,0 +1,11 @@
+.appearance-logo-preview {
+ max-width: 400px;
+ margin-bottom: 20px;
+}
+
+.appearance-light-logo-preview {
+ background-color: $background-color;
+ max-width: 72px;
+ padding: 10px;
+ margin-bottom: 10px;
+}
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 87dd30f4111..28994e60baa 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -1,125 +1,133 @@
.awards {
- @include clearfix;
line-height: 34px;
.emoji-icon {
width: 20px;
height: 20px;
- margin: 7px 0 0 5px;
}
+}
- .award {
- @include border-radius(5px);
-
- border: 1px solid;
- padding: 0px 10px;
- float: left;
- margin-right: 5px;
- border-color: $border-color;
- cursor: pointer;
+.emoji-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ margin-top: 3px;
+ z-index: 1000;
+ min-width: 160px;
+ font-size: 14px;
+ background-color: $award-emoji-menu-bg;
+ border: 1px solid $award-emoji-menu-border;
+ border-radius: $border-radius-base;
+ box-shadow: 0 6px 12px rgba(0,0,0,.175);
+ pointer-events: none;
+ opacity: 0;
+ transform: scale(.2);
+ transform-origin: 0 -45px;
+ transition: all .3s cubic-bezier(.87,-.41,.19,1.44);
+
+ &.is-visible {
+ pointer-events: all;
+ opacity: 1;
+ transform: scale(1);
+ }
- &:hover {
- background-color: #dce0e5;
+ .emoji-menu-content {
+ padding: $gl-padding;
+ width: 300px;
+ height: 300px;
+ overflow-y: scroll;
+
+ input.emoji-search{
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC");
+ background-repeat: no-repeat;
+ background-position: right 5px center;
+ background-size: 16px;
}
+ }
+}
- &.active {
- border-color: $border-gray-light;
- background-color: $gray-light;
-
- &:hover {
- background-color: #dce0e5;
- }
+.emoji-menu-list {
+ list-style: none;
+ padding-left: 0;
+ margin-bottom: 0;
+}
- .counter {
- font-weight: bold;
- }
- }
+.emoji-menu-list-item {
+ padding: 3px;
+ margin-left: 1px;
+ margin-right: 1px;
+}
- .icon {
- float: left;
- margin-right: 10px;
- }
+.emoji-menu-btn {
+ display: block;
+ cursor: pointer;
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ background: none;
+ border: 0;
+ border-radius: $border-radius-base;
+ transition: transform .15s cubic-bezier(.3, 0, .2, 2);
+
+ &:hover {
+ background-color: transparent;
+ outline: 0;
+ transform: scale(1.3);
+ }
- .counter {
- float: left;
- }
+ &:focus,
+ &:active {
+ outline: 0;
}
- .awards-controls {
+ .emoji-icon {
+ display: inline-block;
position: relative;
- margin-left: 10px;
- float: left;
+ top: 3px;
+ }
+}
- .add-award {
- font-size: 24px;
- color: $gl-gray;
- position: relative;
- top: 2px;
+.award-menu-holder {
+ display: inline-block;
+ position: relative;
+}
- &:hover,
- &:link {
- text-decoration: none;
- }
- }
+.award-control {
+ margin-right: 5px;
+ padding-left: 5px;
+ padding-right: 5px;
+ line-height: 20px;
+ outline: 0;
+
+ &.active,
+ &:active {
+ background-color: $white-dark;
+ box-shadow: none;
+ outline: 0;
+ }
- .emoji-menu{
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 1000;
+ &.is-loading {
+ .award-control-icon {
display: none;
- float: left;
- min-width: 160px;
- padding: 5px 0;
- margin: 2px 0 0;
- font-size: 14px;
- text-align: left;
- list-style: none;
- background-color: #fff;
- -webkit-background-clip: padding-box;
- background-clip: padding-box;
- border: 1px solid #ccc;
- border: 1px solid rgba(0,0,0,.15);
- border-radius: 4px;
- -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
- box-shadow: 0 6px 12px rgba(0,0,0,.175);
-
- .emoji-menu-content {
- padding: $gl-padding;
- width: 300px;
- height: 300px;
- overflow-y: scroll;
-
- h5 {
- clear: left;
- }
-
- ul {
- list-style-type: none;
- margin-left: -20px;
- margin-bottom: 20px;
- overflow: auto;
- }
-
- input.emoji-search{
- background: image-url("icon-search.png") 240px no-repeat;
- }
-
- li {
- cursor: pointer;
- width: 30px;
- height: 30px;
- text-align: center;
- float: left;
- margin: 3px;
- list-decorate: none;
- @include border-radius(5px);
-
- &:hover {
- background-color: #ccc;
- }
- }
- }
}
+
+ .award-control-icon-loading {
+ display: block;
+ }
+ }
+
+ .icon,
+ .award-control-icon {
+ float: left;
+ margin-right: 5px;
+ font-size: 20px;
+ }
+
+ .award-control-icon-loading {
+ display: none;
+ }
+
+ .award-control-icon {
+ color: $award-emoji-new-btn-icon-color;
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 3c2997c1d5a..201f3e5ca46 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,6 +1,6 @@
.build-page {
pre.trace {
- background: #111111;
+ background: #111;
color: #fff;
font-family: $monospace_font;
white-space: pre;
@@ -27,10 +27,25 @@
}
.scroll-controls {
- position: fixed;
- bottom: 10px;
- left: 250px;
- z-index: 100;
+ &.affix-top {
+ position: absolute;
+ top: 10px;
+ right: 25px;
+ }
+
+ &.affix-bottom {
+ position: absolute;
+ right: 25px;
+ }
+
+ &.affix {
+ right: 30px;
+ bottom: 15px;
+
+ @media (min-width: $screen-md-min) {
+ right: 26%;
+ }
+ }
a {
display: block;
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 17245d3be7b..971656feb42 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -35,6 +35,8 @@
}
.commit-box {
+ border-top: 1px solid $border-color;
+
.commit-title {
margin: 0;
font-size: 23px;
@@ -53,7 +55,7 @@
padding: 10px 0;
li {
- padding: 3px 0px;
+ padding: 3px 0;
line-height: 20px;
}
}
@@ -88,6 +90,7 @@
position: relative;
font-family: $monospace_font;
$left: 12px;
+ overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
.max-width-marker {
width: 72ch;
color: rgba(0, 0, 0, 0.0);
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index c9dfcff6290..d57be1b2daa 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -9,7 +9,7 @@
.lists-separator {
margin: 10px 0;
- border-color: #DDD;
+ border-color: #ddd;
}
.commits-row {
@@ -28,10 +28,6 @@
}
}
-.commits-feed-holder {
- float: right;
-}
-
li.commit {
list-style: none;
@@ -40,6 +36,10 @@ li.commit {
line-height: 20px;
margin-bottom: 2px;
+ .btn-clipboard {
+ margin-top: -1px;
+ }
+
.notes_count {
float: right;
margin-right: 10px;
@@ -76,7 +76,7 @@ li.commit {
.commit-row-description {
font-size: 14px;
- border-left: 1px solid #EEE;
+ border-left: 1px solid #eee;
padding: 10px 15px;
margin: 5px 0 10px 5px;
background: #f9f9f9;
@@ -122,3 +122,59 @@ li.commit {
color: $gl-gray;
}
}
+
+.divergence-graph {
+ padding: 12px 12px 0 0;
+ float: right;
+
+ .graph-side {
+ position: relative;
+ width: 80px;
+ height: 22px;
+ padding: 5px 0 13px;
+ float: left;
+
+ .bar {
+ position: absolute;
+ height: 4px;
+ background-color: #ccc;
+ }
+
+ .bar-behind {
+ right: 0;
+ border-radius: 3px 0 0 3px;
+ }
+
+ .bar-ahead {
+ left: 0;
+ border-radius: 0 3px 3px 0;
+ }
+
+ .count {
+ padding-top: 6px;
+ padding-bottom: 0;
+ font-size: 12px;
+ color: #333;
+ display: block;
+ }
+
+ .count-behind {
+ padding-right: 4px;
+ text-align: right;
+ }
+
+ .count-ahead {
+ padding-left: 4px;
+ text-align: left;
+ }
+ }
+
+ .graph-separator {
+ position: relative;
+ width: 1px;
+ height: 18px;
+ margin: 5px 0 0;
+ float: left;
+ background-color: #ccc;
+ }
+}
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
index 25a86cd0f94..cf7567513ec 100644
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -11,15 +11,15 @@
}
.dashboard-search-filter {
- padding:5px;
+ padding: 5px;
.search-text-input {
- float:left;
+ float: left;
@extend .col-md-2;
}
.btn {
margin-left: 5px;
- float:left;
+ float: left;
}
}
@@ -40,10 +40,6 @@
.avatar {
@include border-radius(50%);
}
-
- .identicon {
- line-height: 46px;
- }
}
.dash-project-access-icon {
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index deab805dbc2..d3eda1a57e6 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -1,7 +1,5 @@
.detail-page-header {
- margin: -$gl-padding;
- padding: 7px $gl-padding;
- margin-bottom: 0px;
+ padding: 11px 0;
border-bottom: 1px solid $border-color;
color: #5c5d5e;
font-size: 16px;
@@ -14,6 +12,15 @@
.identifier {
color: #5c5d5e;
}
+
+ .issue_created_ago, .author_link {
+ white-space: nowrap;
+ }
+
+ .issue-meta {
+ display: inline-block;
+ line-height: 20px;
+ }
}
.detail-page-description {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index afd6fb73675..d5862a11aca 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1,9 +1,7 @@
// Common
.diff-file {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- border: none;
- border-bottom: 1px solid #E7E9EE;
+ border: 1px solid $border-color;
+ margin-bottom: $gl-padding;
.diff-header {
position: relative;
@@ -23,14 +21,6 @@
}
}
- .diff-controls {
- .btn {
- padding: 0px 10px;
- font-size: 13px;
- line-height: 28px;
- }
- }
-
.commit-short-id {
font-family: $monospace_font;
font-size: smaller;
@@ -39,19 +29,9 @@
.diff-content {
overflow: auto;
overflow-y: hidden;
- background: #FFF;
+ background: #fff;
color: #333;
- .old {
- span.idiff {
- background-color: #f8cbcb;
- }
- }
- .new {
- span.idiff {
- background-color: #a6f3a6;
- }
- }
.unfold {
cursor: pointer;
}
@@ -76,8 +56,9 @@
width: 100%;
font-family: $monospace_font;
border: none;
- margin: 0px;
- padding: 0px;
+ border-collapse: separate;
+ margin: 0;
+ padding: 0;
.line_holder td {
line-height: $code_line_height;
font-size: $code_font_size;
@@ -85,7 +66,7 @@
}
tr.line_holder.parallel {
- .old_line, .new_line, .diff_line {
+ .old_line, .new_line {
min-width: 50px;
}
@@ -94,14 +75,12 @@
}
}
- .old_line, .new_line, .diff_line {
- margin: 0px;
- padding: 0px;
+ .old_line, .new_line {
+ margin: 0;
+ padding: 0;
border: none;
- background: $background-color;
- color: rgba(0, 0, 0, 0.3);
- padding: 0px 5px;
- border-right: 1px solid $border-color;
+ padding: 0 5px;
+ border-right: 1px solid;
text-align: right;
min-width: 35px;
max-width: 50px;
@@ -111,48 +90,16 @@
float: left;
width: 35px;
font-weight: normal;
- color: rgba(0, 0, 0, 0.3);
&:hover {
text-decoration: underline;
}
}
- &.new {
- background: #CFD;
- }
- &.old {
- background: #FDD;
- }
- }
- .diff_line {
- padding: 0;
- }
- .line_holder {
- &.old .old_line,
- &.old .new_line {
- background: #ffdddd;
- border-color: #f1c0c0;
- }
- &.new .old_line,
- &.new .new_line {
- background: #dbffdb;
- border-color: #c1e9c1;
- }
}
.line_content {
display: block;
- margin: 0px;
- padding: 0px 0.5em;
+ margin: 0;
+ padding: 0 0.5em;
border: none;
- &.new {
- background: #eaffea;
- }
- &.old {
- background: #ffecec;
- }
- &.matched {
- color: $border-color;
- background: #fafafa;
- }
&.parallel {
display: table-cell;
}
@@ -171,7 +118,7 @@
background-color: #fff;
line-height: 0;
img {
- border: 1px solid #FFF;
+ border: 1px solid #fff;
background: image-url('trans_bg.gif');
max-width: 100%;
}
@@ -236,7 +183,7 @@
height: 14px;
width: 15px;
position: absolute;
- top: 0px;
+ top: 0;
background: image-url('swipemode_sprites.gif') 0 3px no-repeat;
}
.bottom-handle {
@@ -244,7 +191,7 @@
height: 14px;
width: 15px;
position: absolute;
- bottom: 0px;
+ bottom: 0;
background: image-url('swipemode_sprites.gif') 0 -11px no-repeat;
}
}
@@ -259,8 +206,8 @@
.frame.added, .frame.deleted {
position: absolute;
display: block;
- top: 0px;
- left: 0px;
+ top: 0;
+ left: 0;
}
.controls {
display: block;
@@ -268,7 +215,7 @@
width: 300px;
z-index: 100;
position: absolute;
- bottom: 0px;
+ bottom: 0;
left: 50%;
margin-left: -150px;
@@ -284,11 +231,11 @@
.dragger {
display: block;
position: absolute;
- left: 0px;
- top: 0px;
+ left: 0;
+ top: 0;
height: 14px;
width: 14px;
- background: image-url('onion_skin_sprites.gif') 0px -34px repeat-x;
+ background: image-url('onion_skin_sprites.gif') 0 -34px repeat-x;
cursor: pointer;
}
@@ -296,17 +243,17 @@
display: block;
position: absolute;
top: 2px;
- right: 0px;
+ right: 0;
height: 10px;
width: 10px;
- background: image-url('onion_skin_sprites.gif') -2px 0px no-repeat;
+ background: image-url('onion_skin_sprites.gif') -2px 0 no-repeat;
}
.opaque {
display: block;
position: absolute;
top: 2px;
- left: 0px;
+ left: 0;
height: 10px;
width: 10px;
background: image-url('onion_skin_sprites.gif') -2px -10px no-repeat;
@@ -318,7 +265,7 @@
.view-modes {
padding: 10px;
text-align: center;
- background: #EEE;
+ background: #eee;
ul, li {
list-style: none;
@@ -402,3 +349,23 @@
right: 15px;
}
}
+
+@mixin diff_background($background, $idiff, $border) {
+ background: $background;
+
+ &.line_content span.idiff {
+ background: $idiff;
+ }
+
+ &.diff-line-num {
+ border-color: $border;
+ }
+}
+
+.files {
+ margin-top: -1px;
+
+ .diff-file:last-child {
+ margin-bottom: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 39d916cd336..43be5e38ba8 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -14,9 +14,9 @@
}
.cancel-btn {
- color: #B94A48;
+ color: #b94a48;
&:hover {
- color: #B94A48;
+ color: #b94a48;
}
}
diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/pages/emojis.scss
index 89a94c5a780..b731abc7450 100644
--- a/app/assets/stylesheets/pages/emojis.scss
+++ b/app/assets/stylesheets/pages/emojis.scss
@@ -1,1272 +1,1736 @@
-/*
-File is generated by https://github.com/jakesgordon/sprite-factory and midified manualy
-The source: gemojione gem.
-*/
+.emoji-0023-20E3 { background-position: 0 0; }
+.emoji-002A-20E3 { background-position: -20px 0; }
+.emoji-0030-20E3 { background-position: 0 -20px; }
+.emoji-0031-20E3 { background-position: -20px -20px; }
+.emoji-0032-20E3 { background-position: -40px 0; }
+.emoji-0033-20E3 { background-position: -40px -20px; }
+.emoji-0034-20E3 { background-position: 0 -40px; }
+.emoji-0035-20E3 { background-position: -20px -40px; }
+.emoji-0036-20E3 { background-position: -40px -40px; }
+.emoji-0037-20E3 { background-position: -60px 0; }
+.emoji-0038-20E3 { background-position: -60px -20px; }
+.emoji-0039-20E3 { background-position: -60px -40px; }
+.emoji-00A9 { background-position: 0 -60px; }
+.emoji-00AE { background-position: -20px -60px; }
+.emoji-1F004 { background-position: -40px -60px; }
+.emoji-1F0CF { background-position: -60px -60px; }
+.emoji-1F170 { background-position: -80px 0; }
+.emoji-1F171 { background-position: -80px -20px; }
+.emoji-1F17E { background-position: -80px -40px; }
+.emoji-1F17F { background-position: -80px -60px; }
+.emoji-1F18E { background-position: 0 -80px; }
+.emoji-1F191 { background-position: -20px -80px; }
+.emoji-1F192 { background-position: -40px -80px; }
+.emoji-1F193 { background-position: -60px -80px; }
+.emoji-1F194 { background-position: -80px -80px; }
+.emoji-1F195 { background-position: -100px 0; }
+.emoji-1F196 { background-position: -100px -20px; }
+.emoji-1F197 { background-position: -100px -40px; }
+.emoji-1F198 { background-position: -100px -60px; }
+.emoji-1F199 { background-position: -100px -80px; }
+.emoji-1F19A { background-position: 0 -100px; }
+.emoji-1F1E6-1F1E8 { background-position: -20px -100px; }
+.emoji-1F1E6-1F1E9 { background-position: -40px -100px; }
+.emoji-1F1E6-1F1EA { background-position: -60px -100px; }
+.emoji-1F1E6-1F1EB { background-position: -80px -100px; }
+.emoji-1F1E6-1F1EC { background-position: -100px -100px; }
+.emoji-1F1E6-1F1EE { background-position: -120px 0; }
+.emoji-1F1E6-1F1F1 { background-position: -120px -20px; }
+.emoji-1F1E6-1F1F2 { background-position: -120px -40px; }
+.emoji-1F1E6-1F1F4 { background-position: -120px -60px; }
+.emoji-1F1E6-1F1F6 { background-position: -120px -80px; }
+.emoji-1F1E6-1F1F7 { background-position: -120px -100px; }
+.emoji-1F1E6-1F1F8 { background-position: 0 -120px; }
+.emoji-1F1E6-1F1F9 { background-position: -20px -120px; }
+.emoji-1F1E6-1F1FA { background-position: -40px -120px; }
+.emoji-1F1E6-1F1FC { background-position: -60px -120px; }
+.emoji-1F1E6-1F1FD { background-position: -80px -120px; }
+.emoji-1F1E6-1F1FF { background-position: -100px -120px; }
+.emoji-1F1E7-1F1E6 { background-position: -120px -120px; }
+.emoji-1F1E7-1F1E7 { background-position: -140px 0; }
+.emoji-1F1E7-1F1E9 { background-position: -140px -20px; }
+.emoji-1F1E7-1F1EA { background-position: -140px -40px; }
+.emoji-1F1E7-1F1EB { background-position: -140px -60px; }
+.emoji-1F1E7-1F1EC { background-position: -140px -80px; }
+.emoji-1F1E7-1F1ED { background-position: -140px -100px; }
+.emoji-1F1E7-1F1EE { background-position: -140px -120px; }
+.emoji-1F1E7-1F1EF { background-position: 0 -140px; }
+.emoji-1F1E7-1F1F1 { background-position: -20px -140px; }
+.emoji-1F1E7-1F1F2 { background-position: -40px -140px; }
+.emoji-1F1E7-1F1F3 { background-position: -60px -140px; }
+.emoji-1F1E7-1F1F4 { background-position: -80px -140px; }
+.emoji-1F1E7-1F1F6 { background-position: -100px -140px; }
+.emoji-1F1E7-1F1F7 { background-position: -120px -140px; }
+.emoji-1F1E7-1F1F8 { background-position: -140px -140px; }
+.emoji-1F1E7-1F1F9 { background-position: -160px 0; }
+.emoji-1F1E7-1F1FB { background-position: -160px -20px; }
+.emoji-1F1E7-1F1FC { background-position: -160px -40px; }
+.emoji-1F1E7-1F1FE { background-position: -160px -60px; }
+.emoji-1F1E7-1F1FF { background-position: -160px -80px; }
+.emoji-1F1E8-1F1E6 { background-position: -160px -100px; }
+.emoji-1F1E8-1F1E8 { background-position: -160px -120px; }
+.emoji-1F1E8-1F1E9 { background-position: -160px -140px; }
+.emoji-1F1E8-1F1EB { background-position: 0 -160px; }
+.emoji-1F1E8-1F1EC { background-position: -20px -160px; }
+.emoji-1F1E8-1F1ED { background-position: -40px -160px; }
+.emoji-1F1E8-1F1EE { background-position: -60px -160px; }
+.emoji-1F1E8-1F1F0 { background-position: -80px -160px; }
+.emoji-1F1E8-1F1F1 { background-position: -100px -160px; }
+.emoji-1F1E8-1F1F2 { background-position: -120px -160px; }
+.emoji-1F1E8-1F1F3 { background-position: -140px -160px; }
+.emoji-1F1E8-1F1F4 { background-position: -160px -160px; }
+.emoji-1F1E8-1F1F5 { background-position: -180px 0; }
+.emoji-1F1E8-1F1F7 { background-position: -180px -20px; }
+.emoji-1F1E8-1F1FA { background-position: -180px -40px; }
+.emoji-1F1E8-1F1FB { background-position: -180px -60px; }
+.emoji-1F1E8-1F1FC { background-position: -180px -80px; }
+.emoji-1F1E8-1F1FD { background-position: -180px -100px; }
+.emoji-1F1E8-1F1FE { background-position: -180px -120px; }
+.emoji-1F1E8-1F1FF { background-position: -180px -140px; }
+.emoji-1F1E9-1F1EA { background-position: -180px -160px; }
+.emoji-1F1E9-1F1EC { background-position: 0 -180px; }
+.emoji-1F1E9-1F1EF { background-position: -20px -180px; }
+.emoji-1F1E9-1F1F0 { background-position: -40px -180px; }
+.emoji-1F1E9-1F1F2 { background-position: -60px -180px; }
+.emoji-1F1E9-1F1F4 { background-position: -80px -180px; }
+.emoji-1F1E9-1F1FF { background-position: -100px -180px; }
+.emoji-1F1EA-1F1E6 { background-position: -120px -180px; }
+.emoji-1F1EA-1F1E8 { background-position: -140px -180px; }
+.emoji-1F1EA-1F1EA { background-position: -160px -180px; }
+.emoji-1F1EA-1F1EC { background-position: -180px -180px; }
+.emoji-1F1EA-1F1ED { background-position: -200px 0; }
+.emoji-1F1EA-1F1F7 { background-position: -200px -20px; }
+.emoji-1F1EA-1F1F8 { background-position: -200px -40px; }
+.emoji-1F1EA-1F1F9 { background-position: -200px -60px; }
+.emoji-1F1EA-1F1FA { background-position: -200px -80px; }
+.emoji-1F1EB-1F1EE { background-position: -200px -100px; }
+.emoji-1F1EB-1F1EF { background-position: -200px -120px; }
+.emoji-1F1EB-1F1F0 { background-position: -200px -140px; }
+.emoji-1F1EB-1F1F2 { background-position: -200px -160px; }
+.emoji-1F1EB-1F1F4 { background-position: -200px -180px; }
+.emoji-1F1EB-1F1F7 { background-position: 0 -200px; }
+.emoji-1F1EC-1F1E6 { background-position: -20px -200px; }
+.emoji-1F1EC-1F1E7 { background-position: -40px -200px; }
+.emoji-1F1EC-1F1E9 { background-position: -60px -200px; }
+.emoji-1F1EC-1F1EA { background-position: -80px -200px; }
+.emoji-1F1EC-1F1EB { background-position: -100px -200px; }
+.emoji-1F1EC-1F1EC { background-position: -120px -200px; }
+.emoji-1F1EC-1F1ED { background-position: -140px -200px; }
+.emoji-1F1EC-1F1EE { background-position: -160px -200px; }
+.emoji-1F1EC-1F1F1 { background-position: -180px -200px; }
+.emoji-1F1EC-1F1F2 { background-position: -200px -200px; }
+.emoji-1F1EC-1F1F3 { background-position: -220px 0; }
+.emoji-1F1EC-1F1F5 { background-position: -220px -20px; }
+.emoji-1F1EC-1F1F6 { background-position: -220px -40px; }
+.emoji-1F1EC-1F1F7 { background-position: -220px -60px; }
+.emoji-1F1EC-1F1F8 { background-position: -220px -80px; }
+.emoji-1F1EC-1F1F9 { background-position: -220px -100px; }
+.emoji-1F1EC-1F1FA { background-position: -220px -120px; }
+.emoji-1F1EC-1F1FC { background-position: -220px -140px; }
+.emoji-1F1EC-1F1FE { background-position: -220px -160px; }
+.emoji-1F1ED-1F1F0 { background-position: -220px -180px; }
+.emoji-1F1ED-1F1F2 { background-position: -220px -200px; }
+.emoji-1F1ED-1F1F3 { background-position: 0 -220px; }
+.emoji-1F1ED-1F1F7 { background-position: -20px -220px; }
+.emoji-1F1ED-1F1F9 { background-position: -40px -220px; }
+.emoji-1F1ED-1F1FA { background-position: -60px -220px; }
+.emoji-1F1EE-1F1E8 { background-position: -80px -220px; }
+.emoji-1F1EE-1F1E9 { background-position: -100px -220px; }
+.emoji-1F1EE-1F1EA { background-position: -120px -220px; }
+.emoji-1F1EE-1F1F1 { background-position: -140px -220px; }
+.emoji-1F1EE-1F1F2 { background-position: -160px -220px; }
+.emoji-1F1EE-1F1F3 { background-position: -180px -220px; }
+.emoji-1F1EE-1F1F4 { background-position: -200px -220px; }
+.emoji-1F1EE-1F1F6 { background-position: -220px -220px; }
+.emoji-1F1EE-1F1F7 { background-position: -240px 0; }
+.emoji-1F1EE-1F1F8 { background-position: -240px -20px; }
+.emoji-1F1EE-1F1F9 { background-position: -240px -40px; }
+.emoji-1F1EF-1F1EA { background-position: -240px -60px; }
+.emoji-1F1EF-1F1F2 { background-position: -240px -80px; }
+.emoji-1F1EF-1F1F4 { background-position: -240px -100px; }
+.emoji-1F1EF-1F1F5 { background-position: -240px -120px; }
+.emoji-1F1F0-1F1EA { background-position: -240px -140px; }
+.emoji-1F1F0-1F1EC { background-position: -240px -160px; }
+.emoji-1F1F0-1F1ED { background-position: -240px -180px; }
+.emoji-1F1F0-1F1EE { background-position: -240px -200px; }
+.emoji-1F1F0-1F1F2 { background-position: -240px -220px; }
+.emoji-1F1F0-1F1F3 { background-position: 0 -240px; }
+.emoji-1F1F0-1F1F5 { background-position: -20px -240px; }
+.emoji-1F1F0-1F1F7 { background-position: -40px -240px; }
+.emoji-1F1F0-1F1FC { background-position: -60px -240px; }
+.emoji-1F1F0-1F1FE { background-position: -80px -240px; }
+.emoji-1F1F0-1F1FF { background-position: -100px -240px; }
+.emoji-1F1F1-1F1E6 { background-position: -120px -240px; }
+.emoji-1F1F1-1F1E7 { background-position: -140px -240px; }
+.emoji-1F1F1-1F1E8 { background-position: -160px -240px; }
+.emoji-1F1F1-1F1EE { background-position: -180px -240px; }
+.emoji-1F1F1-1F1F0 { background-position: -200px -240px; }
+.emoji-1F1F1-1F1F7 { background-position: -220px -240px; }
+.emoji-1F1F1-1F1F8 { background-position: -240px -240px; }
+.emoji-1F1F1-1F1F9 { background-position: -260px 0; }
+.emoji-1F1F1-1F1FA { background-position: -260px -20px; }
+.emoji-1F1F1-1F1FB { background-position: -260px -40px; }
+.emoji-1F1F1-1F1FE { background-position: -260px -60px; }
+.emoji-1F1F2-1F1E6 { background-position: -260px -80px; }
+.emoji-1F1F2-1F1E8 { background-position: -260px -100px; }
+.emoji-1F1F2-1F1E9 { background-position: -260px -120px; }
+.emoji-1F1F2-1F1EA { background-position: -260px -140px; }
+.emoji-1F1F2-1F1EB { background-position: -260px -160px; }
+.emoji-1F1F2-1F1EC { background-position: -260px -180px; }
+.emoji-1F1F2-1F1ED { background-position: -260px -200px; }
+.emoji-1F1F2-1F1F0 { background-position: -260px -220px; }
+.emoji-1F1F2-1F1F1 { background-position: -260px -240px; }
+.emoji-1F1F2-1F1F2 { background-position: 0 -260px; }
+.emoji-1F1F2-1F1F3 { background-position: -20px -260px; }
+.emoji-1F1F2-1F1F4 { background-position: -40px -260px; }
+.emoji-1F1F2-1F1F5 { background-position: -60px -260px; }
+.emoji-1F1F2-1F1F6 { background-position: -80px -260px; }
+.emoji-1F1F2-1F1F7 { background-position: -100px -260px; }
+.emoji-1F1F2-1F1F8 { background-position: -120px -260px; }
+.emoji-1F1F2-1F1F9 { background-position: -140px -260px; }
+.emoji-1F1F2-1F1FA { background-position: -160px -260px; }
+.emoji-1F1F2-1F1FB { background-position: -180px -260px; }
+.emoji-1F1F2-1F1FC { background-position: -200px -260px; }
+.emoji-1F1F2-1F1FD { background-position: -220px -260px; }
+.emoji-1F1F2-1F1FE { background-position: -240px -260px; }
+.emoji-1F1F2-1F1FF { background-position: -260px -260px; }
+.emoji-1F1F3-1F1E6 { background-position: -280px 0; }
+.emoji-1F1F3-1F1E8 { background-position: -280px -20px; }
+.emoji-1F1F3-1F1EA { background-position: -280px -40px; }
+.emoji-1F1F3-1F1EB { background-position: -280px -60px; }
+.emoji-1F1F3-1F1EC { background-position: -280px -80px; }
+.emoji-1F1F3-1F1EE { background-position: -280px -100px; }
+.emoji-1F1F3-1F1F1 { background-position: -280px -120px; }
+.emoji-1F1F3-1F1F4 { background-position: -280px -140px; }
+.emoji-1F1F3-1F1F5 { background-position: -280px -160px; }
+.emoji-1F1F3-1F1F7 { background-position: -280px -180px; }
+.emoji-1F1F3-1F1FA { background-position: -280px -200px; }
+.emoji-1F1F3-1F1FF { background-position: -280px -220px; }
+.emoji-1F1F4-1F1F2 { background-position: -280px -240px; }
+.emoji-1F1F5-1F1E6 { background-position: -280px -260px; }
+.emoji-1F1F5-1F1EA { background-position: 0 -280px; }
+.emoji-1F1F5-1F1EB { background-position: -20px -280px; }
+.emoji-1F1F5-1F1EC { background-position: -40px -280px; }
+.emoji-1F1F5-1F1ED { background-position: -60px -280px; }
+.emoji-1F1F5-1F1F0 { background-position: -80px -280px; }
+.emoji-1F1F5-1F1F1 { background-position: -100px -280px; }
+.emoji-1F1F5-1F1F2 { background-position: -120px -280px; }
+.emoji-1F1F5-1F1F3 { background-position: -140px -280px; }
+.emoji-1F1F5-1F1F7 { background-position: -160px -280px; }
+.emoji-1F1F5-1F1F8 { background-position: -180px -280px; }
+.emoji-1F1F5-1F1F9 { background-position: -200px -280px; }
+.emoji-1F1F5-1F1FC { background-position: -220px -280px; }
+.emoji-1F1F5-1F1FE { background-position: -240px -280px; }
+.emoji-1F1F6-1F1E6 { background-position: -260px -280px; }
+.emoji-1F1F7-1F1EA { background-position: -280px -280px; }
+.emoji-1F1F7-1F1F4 { background-position: -300px 0; }
+.emoji-1F1F7-1F1F8 { background-position: -300px -20px; }
+.emoji-1F1F7-1F1FA { background-position: -300px -40px; }
+.emoji-1F1F7-1F1FC { background-position: -300px -60px; }
+.emoji-1F1F8-1F1E6 { background-position: -300px -80px; }
+.emoji-1F1F8-1F1E7 { background-position: -300px -100px; }
+.emoji-1F1F8-1F1E8 { background-position: -300px -120px; }
+.emoji-1F1F8-1F1E9 { background-position: -300px -140px; }
+.emoji-1F1F8-1F1EA { background-position: -300px -160px; }
+.emoji-1F1F8-1F1EC { background-position: -300px -180px; }
+.emoji-1F1F8-1F1ED { background-position: -300px -200px; }
+.emoji-1F1F8-1F1EE { background-position: -300px -220px; }
+.emoji-1F1F8-1F1EF { background-position: -300px -240px; }
+.emoji-1F1F8-1F1F0 { background-position: -300px -260px; }
+.emoji-1F1F8-1F1F1 { background-position: -300px -280px; }
+.emoji-1F1F8-1F1F2 { background-position: 0 -300px; }
+.emoji-1F1F8-1F1F3 { background-position: -20px -300px; }
+.emoji-1F1F8-1F1F4 { background-position: -40px -300px; }
+.emoji-1F1F8-1F1F7 { background-position: -60px -300px; }
+.emoji-1F1F8-1F1F8 { background-position: -80px -300px; }
+.emoji-1F1F8-1F1F9 { background-position: -100px -300px; }
+.emoji-1F1F8-1F1FB { background-position: -120px -300px; }
+.emoji-1F1F8-1F1FD { background-position: -140px -300px; }
+.emoji-1F1F8-1F1FE { background-position: -160px -300px; }
+.emoji-1F1F8-1F1FF { background-position: -180px -300px; }
+.emoji-1F1F9-1F1E6 { background-position: -200px -300px; }
+.emoji-1F1F9-1F1E8 { background-position: -220px -300px; }
+.emoji-1F1F9-1F1E9 { background-position: -240px -300px; }
+.emoji-1F1F9-1F1EB { background-position: -260px -300px; }
+.emoji-1F1F9-1F1EC { background-position: -280px -300px; }
+.emoji-1F1F9-1F1ED { background-position: -300px -300px; }
+.emoji-1F1F9-1F1EF { background-position: -320px 0; }
+.emoji-1F1F9-1F1F0 { background-position: -320px -20px; }
+.emoji-1F1F9-1F1F1 { background-position: -320px -40px; }
+.emoji-1F1F9-1F1F2 { background-position: -320px -60px; }
+.emoji-1F1F9-1F1F3 { background-position: -320px -80px; }
+.emoji-1F1F9-1F1F4 { background-position: -320px -100px; }
+.emoji-1F1F9-1F1F7 { background-position: -320px -120px; }
+.emoji-1F1F9-1F1F9 { background-position: -320px -140px; }
+.emoji-1F1F9-1F1FB { background-position: -320px -160px; }
+.emoji-1F1F9-1F1FC { background-position: -320px -180px; }
+.emoji-1F1F9-1F1FF { background-position: -320px -200px; }
+.emoji-1F1FA-1F1E6 { background-position: -320px -220px; }
+.emoji-1F1FA-1F1EC { background-position: -320px -240px; }
+.emoji-1F1FA-1F1F2 { background-position: -320px -260px; }
+.emoji-1F1FA-1F1F8 { background-position: -320px -280px; }
+.emoji-1F1FA-1F1FE { background-position: -320px -300px; }
+.emoji-1F1FA-1F1FF { background-position: 0 -320px; }
+.emoji-1F1FB-1F1E6 { background-position: -20px -320px; }
+.emoji-1F1FB-1F1E8 { background-position: -40px -320px; }
+.emoji-1F1FB-1F1EA { background-position: -60px -320px; }
+.emoji-1F1FB-1F1EC { background-position: -80px -320px; }
+.emoji-1F1FB-1F1EE { background-position: -100px -320px; }
+.emoji-1F1FB-1F1F3 { background-position: -120px -320px; }
+.emoji-1F1FB-1F1FA { background-position: -140px -320px; }
+.emoji-1F1FC-1F1EB { background-position: -160px -320px; }
+.emoji-1F1FC-1F1F8 { background-position: -180px -320px; }
+.emoji-1F1FD-1F1F0 { background-position: -200px -320px; }
+.emoji-1F1FE-1F1EA { background-position: -220px -320px; }
+.emoji-1F1FE-1F1F9 { background-position: -240px -320px; }
+.emoji-1F1FF-1F1E6 { background-position: -260px -320px; }
+.emoji-1F1FF-1F1F2 { background-position: -280px -320px; }
+.emoji-1F1FF-1F1FC { background-position: -300px -320px; }
+.emoji-1F201 { background-position: -320px -320px; }
+.emoji-1F202 { background-position: -340px 0; }
+.emoji-1F21A { background-position: -340px -20px; }
+.emoji-1F22F { background-position: -340px -40px; }
+.emoji-1F232 { background-position: -340px -60px; }
+.emoji-1F233 { background-position: -340px -80px; }
+.emoji-1F234 { background-position: -340px -100px; }
+.emoji-1F235 { background-position: -340px -120px; }
+.emoji-1F236 { background-position: -340px -140px; }
+.emoji-1F237 { background-position: -340px -160px; }
+.emoji-1F238 { background-position: -340px -180px; }
+.emoji-1F239 { background-position: -340px -200px; }
+.emoji-1F23A { background-position: -340px -220px; }
+.emoji-1F250 { background-position: -340px -240px; }
+.emoji-1F251 { background-position: -340px -260px; }
+.emoji-1F300 { background-position: -340px -280px; }
+.emoji-1F301 { background-position: -340px -300px; }
+.emoji-1F302 { background-position: -340px -320px; }
+.emoji-1F303 { background-position: 0 -340px; }
+.emoji-1F304 { background-position: -20px -340px; }
+.emoji-1F305 { background-position: -40px -340px; }
+.emoji-1F306 { background-position: -60px -340px; }
+.emoji-1F307 { background-position: -80px -340px; }
+.emoji-1F308 { background-position: -100px -340px; }
+.emoji-1F309 { background-position: -120px -340px; }
+.emoji-1F30A { background-position: -140px -340px; }
+.emoji-1F30B { background-position: -160px -340px; }
+.emoji-1F30C { background-position: -180px -340px; }
+.emoji-1F30D { background-position: -200px -340px; }
+.emoji-1F30E { background-position: -220px -340px; }
+.emoji-1F30F { background-position: -240px -340px; }
+.emoji-1F310 { background-position: -260px -340px; }
+.emoji-1F311 { background-position: -280px -340px; }
+.emoji-1F312 { background-position: -300px -340px; }
+.emoji-1F313 { background-position: -320px -340px; }
+.emoji-1F314 { background-position: -340px -340px; }
+.emoji-1F315 { background-position: -360px 0; }
+.emoji-1F316 { background-position: -360px -20px; }
+.emoji-1F317 { background-position: -360px -40px; }
+.emoji-1F318 { background-position: -360px -60px; }
+.emoji-1F319 { background-position: -360px -80px; }
+.emoji-1F31A { background-position: -360px -100px; }
+.emoji-1F31B { background-position: -360px -120px; }
+.emoji-1F31C { background-position: -360px -140px; }
+.emoji-1F31D { background-position: -360px -160px; }
+.emoji-1F31E { background-position: -360px -180px; }
+.emoji-1F31F { background-position: -360px -200px; }
+.emoji-1F320 { background-position: -360px -220px; }
+.emoji-1F321 { background-position: -360px -240px; }
+.emoji-1F324 { background-position: -360px -260px; }
+.emoji-1F325 { background-position: -360px -280px; }
+.emoji-1F326 { background-position: -360px -300px; }
+.emoji-1F327 { background-position: -360px -320px; }
+.emoji-1F328 { background-position: -360px -340px; }
+.emoji-1F329 { background-position: 0 -360px; }
+.emoji-1F32A { background-position: -20px -360px; }
+.emoji-1F32B { background-position: -40px -360px; }
+.emoji-1F32C { background-position: -60px -360px; }
+.emoji-1F32D { background-position: -80px -360px; }
+.emoji-1F32E { background-position: -100px -360px; }
+.emoji-1F32F { background-position: -120px -360px; }
+.emoji-1F330 { background-position: -140px -360px; }
+.emoji-1F331 { background-position: -160px -360px; }
+.emoji-1F332 { background-position: -180px -360px; }
+.emoji-1F333 { background-position: -200px -360px; }
+.emoji-1F334 { background-position: -220px -360px; }
+.emoji-1F335 { background-position: -240px -360px; }
+.emoji-1F336 { background-position: -260px -360px; }
+.emoji-1F337 { background-position: -280px -360px; }
+.emoji-1F338 { background-position: -300px -360px; }
+.emoji-1F339 { background-position: -320px -360px; }
+.emoji-1F33A { background-position: -340px -360px; }
+.emoji-1F33B { background-position: -360px -360px; }
+.emoji-1F33C { background-position: -380px 0; }
+.emoji-1F33D { background-position: -380px -20px; }
+.emoji-1F33E { background-position: -380px -40px; }
+.emoji-1F33F { background-position: -380px -60px; }
+.emoji-1F340 { background-position: -380px -80px; }
+.emoji-1F341 { background-position: -380px -100px; }
+.emoji-1F342 { background-position: -380px -120px; }
+.emoji-1F343 { background-position: -380px -140px; }
+.emoji-1F344 { background-position: -380px -160px; }
+.emoji-1F345 { background-position: -380px -180px; }
+.emoji-1F346 { background-position: -380px -200px; }
+.emoji-1F347 { background-position: -380px -220px; }
+.emoji-1F348 { background-position: -380px -240px; }
+.emoji-1F349 { background-position: -380px -260px; }
+.emoji-1F34A { background-position: -380px -280px; }
+.emoji-1F34B { background-position: -380px -300px; }
+.emoji-1F34C { background-position: -380px -320px; }
+.emoji-1F34D { background-position: -380px -340px; }
+.emoji-1F34E { background-position: -380px -360px; }
+.emoji-1F34F { background-position: 0 -380px; }
+.emoji-1F350 { background-position: -20px -380px; }
+.emoji-1F351 { background-position: -40px -380px; }
+.emoji-1F352 { background-position: -60px -380px; }
+.emoji-1F353 { background-position: -80px -380px; }
+.emoji-1F354 { background-position: -100px -380px; }
+.emoji-1F355 { background-position: -120px -380px; }
+.emoji-1F356 { background-position: -140px -380px; }
+.emoji-1F357 { background-position: -160px -380px; }
+.emoji-1F358 { background-position: -180px -380px; }
+.emoji-1F359 { background-position: -200px -380px; }
+.emoji-1F35A { background-position: -220px -380px; }
+.emoji-1F35B { background-position: -240px -380px; }
+.emoji-1F35C { background-position: -260px -380px; }
+.emoji-1F35D { background-position: -280px -380px; }
+.emoji-1F35E { background-position: -300px -380px; }
+.emoji-1F35F { background-position: -320px -380px; }
+.emoji-1F360 { background-position: -340px -380px; }
+.emoji-1F361 { background-position: -360px -380px; }
+.emoji-1F362 { background-position: -380px -380px; }
+.emoji-1F363 { background-position: -400px 0; }
+.emoji-1F364 { background-position: -400px -20px; }
+.emoji-1F365 { background-position: -400px -40px; }
+.emoji-1F366 { background-position: -400px -60px; }
+.emoji-1F367 { background-position: -400px -80px; }
+.emoji-1F368 { background-position: -400px -100px; }
+.emoji-1F369 { background-position: -400px -120px; }
+.emoji-1F36A { background-position: -400px -140px; }
+.emoji-1F36B { background-position: -400px -160px; }
+.emoji-1F36C { background-position: -400px -180px; }
+.emoji-1F36D { background-position: -400px -200px; }
+.emoji-1F36E { background-position: -400px -220px; }
+.emoji-1F36F { background-position: -400px -240px; }
+.emoji-1F370 { background-position: -400px -260px; }
+.emoji-1F371 { background-position: -400px -280px; }
+.emoji-1F372 { background-position: -400px -300px; }
+.emoji-1F373 { background-position: -400px -320px; }
+.emoji-1F374 { background-position: -400px -340px; }
+.emoji-1F375 { background-position: -400px -360px; }
+.emoji-1F376 { background-position: -400px -380px; }
+.emoji-1F377 { background-position: 0 -400px; }
+.emoji-1F378 { background-position: -20px -400px; }
+.emoji-1F379 { background-position: -40px -400px; }
+.emoji-1F37A { background-position: -60px -400px; }
+.emoji-1F37B { background-position: -80px -400px; }
+.emoji-1F37C { background-position: -100px -400px; }
+.emoji-1F37D { background-position: -120px -400px; }
+.emoji-1F37E { background-position: -140px -400px; }
+.emoji-1F37F { background-position: -160px -400px; }
+.emoji-1F380 { background-position: -180px -400px; }
+.emoji-1F381 { background-position: -200px -400px; }
+.emoji-1F382 { background-position: -220px -400px; }
+.emoji-1F383 { background-position: -240px -400px; }
+.emoji-1F384 { background-position: -260px -400px; }
+.emoji-1F385 { background-position: -280px -400px; }
+.emoji-1F385-1F3FB { background-position: -300px -400px; }
+.emoji-1F385-1F3FC { background-position: -320px -400px; }
+.emoji-1F385-1F3FD { background-position: -340px -400px; }
+.emoji-1F385-1F3FE { background-position: -360px -400px; }
+.emoji-1F385-1F3FF { background-position: -380px -400px; }
+.emoji-1F386 { background-position: -400px -400px; }
+.emoji-1F387 { background-position: -420px 0; }
+.emoji-1F388 { background-position: -420px -20px; }
+.emoji-1F389 { background-position: -420px -40px; }
+.emoji-1F38A { background-position: -420px -60px; }
+.emoji-1F38B { background-position: -420px -80px; }
+.emoji-1F38C { background-position: -420px -100px; }
+.emoji-1F38D { background-position: -420px -120px; }
+.emoji-1F38E { background-position: -420px -140px; }
+.emoji-1F38F { background-position: -420px -160px; }
+.emoji-1F390 { background-position: -420px -180px; }
+.emoji-1F391 { background-position: -420px -200px; }
+.emoji-1F392 { background-position: -420px -220px; }
+.emoji-1F393 { background-position: -420px -240px; }
+.emoji-1F394 { background-position: -420px -260px; }
+.emoji-1F395 { background-position: -420px -280px; }
+.emoji-1F396 { background-position: -420px -300px; }
+.emoji-1F397 { background-position: -420px -320px; }
+.emoji-1F398 { background-position: -420px -340px; }
+.emoji-1F399 { background-position: -420px -360px; }
+.emoji-1F39A { background-position: -420px -380px; }
+.emoji-1F39B { background-position: -420px -400px; }
+.emoji-1F39C { background-position: 0 -420px; }
+.emoji-1F39D { background-position: -20px -420px; }
+.emoji-1F39E { background-position: -40px -420px; }
+.emoji-1F39F { background-position: -60px -420px; }
+.emoji-1F3A0 { background-position: -80px -420px; }
+.emoji-1F3A1 { background-position: -100px -420px; }
+.emoji-1F3A2 { background-position: -120px -420px; }
+.emoji-1F3A3 { background-position: -140px -420px; }
+.emoji-1F3A4 { background-position: -160px -420px; }
+.emoji-1F3A5 { background-position: -180px -420px; }
+.emoji-1F3A6 { background-position: -200px -420px; }
+.emoji-1F3A7 { background-position: -220px -420px; }
+.emoji-1F3A8 { background-position: -240px -420px; }
+.emoji-1F3A9 { background-position: -260px -420px; }
+.emoji-1F3AA { background-position: -280px -420px; }
+.emoji-1F3AB { background-position: -300px -420px; }
+.emoji-1F3AC { background-position: -320px -420px; }
+.emoji-1F3AD { background-position: -340px -420px; }
+.emoji-1F3AE { background-position: -360px -420px; }
+.emoji-1F3AF { background-position: -380px -420px; }
+.emoji-1F3B0 { background-position: -400px -420px; }
+.emoji-1F3B1 { background-position: -420px -420px; }
+.emoji-1F3B2 { background-position: -440px 0; }
+.emoji-1F3B3 { background-position: -440px -20px; }
+.emoji-1F3B4 { background-position: -440px -40px; }
+.emoji-1F3B5 { background-position: -440px -60px; }
+.emoji-1F3B6 { background-position: -440px -80px; }
+.emoji-1F3B7 { background-position: -440px -100px; }
+.emoji-1F3B8 { background-position: -440px -120px; }
+.emoji-1F3B9 { background-position: -440px -140px; }
+.emoji-1F3BA { background-position: -440px -160px; }
+.emoji-1F3BB { background-position: -440px -180px; }
+.emoji-1F3BC { background-position: -440px -200px; }
+.emoji-1F3BD { background-position: -440px -220px; }
+.emoji-1F3BE { background-position: -440px -240px; }
+.emoji-1F3BF { background-position: -440px -260px; }
+.emoji-1F3C0 { background-position: -440px -280px; }
+.emoji-1F3C1 { background-position: -440px -300px; }
+.emoji-1F3C2 { background-position: -440px -320px; }
+.emoji-1F3C3 { background-position: -440px -340px; }
+.emoji-1F3C3-1F3FB { background-position: -440px -360px; }
+.emoji-1F3C3-1F3FC { background-position: -440px -380px; }
+.emoji-1F3C3-1F3FD { background-position: -440px -400px; }
+.emoji-1F3C3-1F3FE { background-position: -440px -420px; }
+.emoji-1F3C3-1F3FF { background-position: 0 -440px; }
+.emoji-1F3C4 { background-position: -20px -440px; }
+.emoji-1F3C4-1F3FB { background-position: -40px -440px; }
+.emoji-1F3C4-1F3FC { background-position: -60px -440px; }
+.emoji-1F3C4-1F3FD { background-position: -80px -440px; }
+.emoji-1F3C4-1F3FE { background-position: -100px -440px; }
+.emoji-1F3C4-1F3FF { background-position: -120px -440px; }
+.emoji-1F3C5 { background-position: -140px -440px; }
+.emoji-1F3C6 { background-position: -160px -440px; }
+.emoji-1F3C7 { background-position: -180px -440px; }
+.emoji-1F3C7-1F3FB { background-position: -200px -440px; }
+.emoji-1F3C7-1F3FC { background-position: -220px -440px; }
+.emoji-1F3C7-1F3FD { background-position: -240px -440px; }
+.emoji-1F3C7-1F3FE { background-position: -260px -440px; }
+.emoji-1F3C7-1F3FF { background-position: -280px -440px; }
+.emoji-1F3C8 { background-position: -300px -440px; }
+.emoji-1F3C9 { background-position: -320px -440px; }
+.emoji-1F3CA { background-position: -340px -440px; }
+.emoji-1F3CA-1F3FB { background-position: -360px -440px; }
+.emoji-1F3CA-1F3FC { background-position: -380px -440px; }
+.emoji-1F3CA-1F3FD { background-position: -400px -440px; }
+.emoji-1F3CA-1F3FE { background-position: -420px -440px; }
+.emoji-1F3CA-1F3FF { background-position: -440px -440px; }
+.emoji-1F3CB { background-position: -460px 0; }
+.emoji-1F3CB-1F3FB { background-position: -460px -20px; }
+.emoji-1F3CB-1F3FC { background-position: -460px -40px; }
+.emoji-1F3CB-1F3FD { background-position: -460px -60px; }
+.emoji-1F3CB-1F3FE { background-position: -460px -80px; }
+.emoji-1F3CB-1F3FF { background-position: -460px -100px; }
+.emoji-1F3CC { background-position: -460px -120px; }
+.emoji-1F3CD { background-position: -460px -140px; }
+.emoji-1F3CE { background-position: -460px -160px; }
+.emoji-1F3CF { background-position: -460px -180px; }
+.emoji-1F3D0 { background-position: -460px -200px; }
+.emoji-1F3D1 { background-position: -460px -220px; }
+.emoji-1F3D2 { background-position: -460px -240px; }
+.emoji-1F3D3 { background-position: -460px -260px; }
+.emoji-1F3D4 { background-position: -460px -280px; }
+.emoji-1F3D5 { background-position: -460px -300px; }
+.emoji-1F3D6 { background-position: -460px -320px; }
+.emoji-1F3D7 { background-position: -460px -340px; }
+.emoji-1F3D8 { background-position: -460px -360px; }
+.emoji-1F3D9 { background-position: -460px -380px; }
+.emoji-1F3DA { background-position: -460px -400px; }
+.emoji-1F3DB { background-position: -460px -420px; }
+.emoji-1F3DC { background-position: -460px -440px; }
+.emoji-1F3DD { background-position: 0 -460px; }
+.emoji-1F3DE { background-position: -20px -460px; }
+.emoji-1F3DF { background-position: -40px -460px; }
+.emoji-1F3E0 { background-position: -60px -460px; }
+.emoji-1F3E1 { background-position: -80px -460px; }
+.emoji-1F3E2 { background-position: -100px -460px; }
+.emoji-1F3E3 { background-position: -120px -460px; }
+.emoji-1F3E4 { background-position: -140px -460px; }
+.emoji-1F3E5 { background-position: -160px -460px; }
+.emoji-1F3E6 { background-position: -180px -460px; }
+.emoji-1F3E7 { background-position: -200px -460px; }
+.emoji-1F3E8 { background-position: -220px -460px; }
+.emoji-1F3E9 { background-position: -240px -460px; }
+.emoji-1F3EA { background-position: -260px -460px; }
+.emoji-1F3EB { background-position: -280px -460px; }
+.emoji-1F3EC { background-position: -300px -460px; }
+.emoji-1F3ED { background-position: -320px -460px; }
+.emoji-1F3EE { background-position: -340px -460px; }
+.emoji-1F3EF { background-position: -360px -460px; }
+.emoji-1F3F0 { background-position: -380px -460px; }
+.emoji-1F3F1 { background-position: -400px -460px; }
+.emoji-1F3F2 { background-position: -420px -460px; }
+.emoji-1F3F3 { background-position: -440px -460px; }
+.emoji-1F3F4 { background-position: -460px -460px; }
+.emoji-1F3F5 { background-position: -480px 0; }
+.emoji-1F3F6 { background-position: -480px -20px; }
+.emoji-1F3F7 { background-position: -480px -40px; }
+.emoji-1F3F8 { background-position: -480px -60px; }
+.emoji-1F3F9 { background-position: -480px -80px; }
+.emoji-1F3FA { background-position: -480px -100px; }
+.emoji-1F3FB { background-position: -480px -120px; }
+.emoji-1F3FC { background-position: -480px -140px; }
+.emoji-1F3FD { background-position: -480px -160px; }
+.emoji-1F3FE { background-position: -480px -180px; }
+.emoji-1F3FF { background-position: -480px -200px; }
+.emoji-1F400 { background-position: -480px -220px; }
+.emoji-1F401 { background-position: -480px -240px; }
+.emoji-1F402 { background-position: -480px -260px; }
+.emoji-1F403 { background-position: -480px -280px; }
+.emoji-1F404 { background-position: -480px -300px; }
+.emoji-1F405 { background-position: -480px -320px; }
+.emoji-1F406 { background-position: -480px -340px; }
+.emoji-1F407 { background-position: -480px -360px; }
+.emoji-1F408 { background-position: -480px -380px; }
+.emoji-1F409 { background-position: -480px -400px; }
+.emoji-1F40A { background-position: -480px -420px; }
+.emoji-1F40B { background-position: -480px -440px; }
+.emoji-1F40C { background-position: -480px -460px; }
+.emoji-1F40D { background-position: 0 -480px; }
+.emoji-1F40E { background-position: -20px -480px; }
+.emoji-1F40F { background-position: -40px -480px; }
+.emoji-1F410 { background-position: -60px -480px; }
+.emoji-1F411 { background-position: -80px -480px; }
+.emoji-1F412 { background-position: -100px -480px; }
+.emoji-1F413 { background-position: -120px -480px; }
+.emoji-1F414 { background-position: -140px -480px; }
+.emoji-1F415 { background-position: -160px -480px; }
+.emoji-1F416 { background-position: -180px -480px; }
+.emoji-1F417 { background-position: -200px -480px; }
+.emoji-1F418 { background-position: -220px -480px; }
+.emoji-1F419 { background-position: -240px -480px; }
+.emoji-1F41A { background-position: -260px -480px; }
+.emoji-1F41B { background-position: -280px -480px; }
+.emoji-1F41C { background-position: -300px -480px; }
+.emoji-1F41D { background-position: -320px -480px; }
+.emoji-1F41E { background-position: -340px -480px; }
+.emoji-1F41F { background-position: -360px -480px; }
+.emoji-1F420 { background-position: -380px -480px; }
+.emoji-1F421 { background-position: -400px -480px; }
+.emoji-1F422 { background-position: -420px -480px; }
+.emoji-1F423 { background-position: -440px -480px; }
+.emoji-1F424 { background-position: -460px -480px; }
+.emoji-1F425 { background-position: -480px -480px; }
+.emoji-1F426 { background-position: -500px 0; }
+.emoji-1F427 { background-position: -500px -20px; }
+.emoji-1F428 { background-position: -500px -40px; }
+.emoji-1F429 { background-position: -500px -60px; }
+.emoji-1F42A { background-position: -500px -80px; }
+.emoji-1F42B { background-position: -500px -100px; }
+.emoji-1F42C { background-position: -500px -120px; }
+.emoji-1F42D { background-position: -500px -140px; }
+.emoji-1F42E { background-position: -500px -160px; }
+.emoji-1F42F { background-position: -500px -180px; }
+.emoji-1F430 { background-position: -500px -200px; }
+.emoji-1F431 { background-position: -500px -220px; }
+.emoji-1F432 { background-position: -500px -240px; }
+.emoji-1F433 { background-position: -500px -260px; }
+.emoji-1F434 { background-position: -500px -280px; }
+.emoji-1F435 { background-position: -500px -300px; }
+.emoji-1F436 { background-position: -500px -320px; }
+.emoji-1F437 { background-position: -500px -340px; }
+.emoji-1F438 { background-position: -500px -360px; }
+.emoji-1F439 { background-position: -500px -380px; }
+.emoji-1F43A { background-position: -500px -400px; }
+.emoji-1F43B { background-position: -500px -420px; }
+.emoji-1F43C { background-position: -500px -440px; }
+.emoji-1F43D { background-position: -500px -460px; }
+.emoji-1F43E { background-position: -500px -480px; }
+.emoji-1F43F { background-position: 0 -500px; }
+.emoji-1F440 { background-position: -20px -500px; }
+.emoji-1F441 { background-position: -40px -500px; }
+.emoji-1F441-1F5E8 { background-position: -60px -500px; }
+.emoji-1F442 { background-position: -80px -500px; }
+.emoji-1F442-1F3FB { background-position: -100px -500px; }
+.emoji-1F442-1F3FC { background-position: -120px -500px; }
+.emoji-1F442-1F3FD { background-position: -140px -500px; }
+.emoji-1F442-1F3FE { background-position: -160px -500px; }
+.emoji-1F442-1F3FF { background-position: -180px -500px; }
+.emoji-1F443 { background-position: -200px -500px; }
+.emoji-1F443-1F3FB { background-position: -220px -500px; }
+.emoji-1F443-1F3FC { background-position: -240px -500px; }
+.emoji-1F443-1F3FD { background-position: -260px -500px; }
+.emoji-1F443-1F3FE { background-position: -280px -500px; }
+.emoji-1F443-1F3FF { background-position: -300px -500px; }
+.emoji-1F444 { background-position: -320px -500px; }
+.emoji-1F445 { background-position: -340px -500px; }
+.emoji-1F446 { background-position: -360px -500px; }
+.emoji-1F446-1F3FB { background-position: -380px -500px; }
+.emoji-1F446-1F3FC { background-position: -400px -500px; }
+.emoji-1F446-1F3FD { background-position: -420px -500px; }
+.emoji-1F446-1F3FE { background-position: -440px -500px; }
+.emoji-1F446-1F3FF { background-position: -460px -500px; }
+.emoji-1F447 { background-position: -480px -500px; }
+.emoji-1F447-1F3FB { background-position: -500px -500px; }
+.emoji-1F447-1F3FC { background-position: -520px 0; }
+.emoji-1F447-1F3FD { background-position: -520px -20px; }
+.emoji-1F447-1F3FE { background-position: -520px -40px; }
+.emoji-1F447-1F3FF { background-position: -520px -60px; }
+.emoji-1F448 { background-position: -520px -80px; }
+.emoji-1F448-1F3FB { background-position: -520px -100px; }
+.emoji-1F448-1F3FC { background-position: -520px -120px; }
+.emoji-1F448-1F3FD { background-position: -520px -140px; }
+.emoji-1F448-1F3FE { background-position: -520px -160px; }
+.emoji-1F448-1F3FF { background-position: -520px -180px; }
+.emoji-1F449 { background-position: -520px -200px; }
+.emoji-1F449-1F3FB { background-position: -520px -220px; }
+.emoji-1F449-1F3FC { background-position: -520px -240px; }
+.emoji-1F449-1F3FD { background-position: -520px -260px; }
+.emoji-1F449-1F3FE { background-position: -520px -280px; }
+.emoji-1F449-1F3FF { background-position: -520px -300px; }
+.emoji-1F44A { background-position: -520px -320px; }
+.emoji-1F44A-1F3FB { background-position: -520px -340px; }
+.emoji-1F44A-1F3FC { background-position: -520px -360px; }
+.emoji-1F44A-1F3FD { background-position: -520px -380px; }
+.emoji-1F44A-1F3FE { background-position: -520px -400px; }
+.emoji-1F44A-1F3FF { background-position: -520px -420px; }
+.emoji-1F44B { background-position: -520px -440px; }
+.emoji-1F44B-1F3FB { background-position: -520px -460px; }
+.emoji-1F44B-1F3FC { background-position: -520px -480px; }
+.emoji-1F44B-1F3FD { background-position: -520px -500px; }
+.emoji-1F44B-1F3FE { background-position: 0 -520px; }
+.emoji-1F44B-1F3FF { background-position: -20px -520px; }
+.emoji-1F44C { background-position: -40px -520px; }
+.emoji-1F44C-1F3FB { background-position: -60px -520px; }
+.emoji-1F44C-1F3FC { background-position: -80px -520px; }
+.emoji-1F44C-1F3FD { background-position: -100px -520px; }
+.emoji-1F44C-1F3FE { background-position: -120px -520px; }
+.emoji-1F44C-1F3FF { background-position: -140px -520px; }
+.emoji-1F44D { background-position: -160px -520px; }
+.emoji-1F44D-1F3FB { background-position: -180px -520px; }
+.emoji-1F44D-1F3FC { background-position: -200px -520px; }
+.emoji-1F44D-1F3FD { background-position: -220px -520px; }
+.emoji-1F44D-1F3FE { background-position: -240px -520px; }
+.emoji-1F44D-1F3FF { background-position: -260px -520px; }
+.emoji-1F44E { background-position: -280px -520px; }
+.emoji-1F44E-1F3FB { background-position: -300px -520px; }
+.emoji-1F44E-1F3FC { background-position: -320px -520px; }
+.emoji-1F44E-1F3FD { background-position: -340px -520px; }
+.emoji-1F44E-1F3FE { background-position: -360px -520px; }
+.emoji-1F44E-1F3FF { background-position: -380px -520px; }
+.emoji-1F44F { background-position: -400px -520px; }
+.emoji-1F44F-1F3FB { background-position: -420px -520px; }
+.emoji-1F44F-1F3FC { background-position: -440px -520px; }
+.emoji-1F44F-1F3FD { background-position: -460px -520px; }
+.emoji-1F44F-1F3FE { background-position: -480px -520px; }
+.emoji-1F44F-1F3FF { background-position: -500px -520px; }
+.emoji-1F450 { background-position: -520px -520px; }
+.emoji-1F450-1F3FB { background-position: -540px 0; }
+.emoji-1F450-1F3FC { background-position: -540px -20px; }
+.emoji-1F450-1F3FD { background-position: -540px -40px; }
+.emoji-1F450-1F3FE { background-position: -540px -60px; }
+.emoji-1F450-1F3FF { background-position: -540px -80px; }
+.emoji-1F451 { background-position: -540px -100px; }
+.emoji-1F452 { background-position: -540px -120px; }
+.emoji-1F453 { background-position: -540px -140px; }
+.emoji-1F454 { background-position: -540px -160px; }
+.emoji-1F455 { background-position: -540px -180px; }
+.emoji-1F456 { background-position: -540px -200px; }
+.emoji-1F457 { background-position: -540px -220px; }
+.emoji-1F458 { background-position: -540px -240px; }
+.emoji-1F459 { background-position: -540px -260px; }
+.emoji-1F45A { background-position: -540px -280px; }
+.emoji-1F45B { background-position: -540px -300px; }
+.emoji-1F45C { background-position: -540px -320px; }
+.emoji-1F45D { background-position: -540px -340px; }
+.emoji-1F45E { background-position: -540px -360px; }
+.emoji-1F45F { background-position: -540px -380px; }
+.emoji-1F460 { background-position: -540px -400px; }
+.emoji-1F461 { background-position: -540px -420px; }
+.emoji-1F462 { background-position: -540px -440px; }
+.emoji-1F463 { background-position: -540px -460px; }
+.emoji-1F464 { background-position: -540px -480px; }
+.emoji-1F465 { background-position: -540px -500px; }
+.emoji-1F466 { background-position: -540px -520px; }
+.emoji-1F466-1F3FB { background-position: 0 -540px; }
+.emoji-1F466-1F3FC { background-position: -20px -540px; }
+.emoji-1F466-1F3FD { background-position: -40px -540px; }
+.emoji-1F466-1F3FE { background-position: -60px -540px; }
+.emoji-1F466-1F3FF { background-position: -80px -540px; }
+.emoji-1F467 { background-position: -100px -540px; }
+.emoji-1F467-1F3FB { background-position: -120px -540px; }
+.emoji-1F467-1F3FC { background-position: -140px -540px; }
+.emoji-1F467-1F3FD { background-position: -160px -540px; }
+.emoji-1F467-1F3FE { background-position: -180px -540px; }
+.emoji-1F467-1F3FF { background-position: -200px -540px; }
+.emoji-1F468 { background-position: -220px -540px; }
+.emoji-1F468-1F3FB { background-position: -240px -540px; }
+.emoji-1F468-1F3FC { background-position: -260px -540px; }
+.emoji-1F468-1F3FD { background-position: -280px -540px; }
+.emoji-1F468-1F3FE { background-position: -300px -540px; }
+.emoji-1F468-1F3FF { background-position: -320px -540px; }
+.emoji-1F468-1F468-1F466 { background-position: -340px -540px; }
+.emoji-1F468-1F468-1F466-1F466 { background-position: -360px -540px; }
+.emoji-1F468-1F468-1F467 { background-position: -380px -540px; }
+.emoji-1F468-1F468-1F467-1F466 { background-position: -400px -540px; }
+.emoji-1F468-1F468-1F467-1F467 { background-position: -420px -540px; }
+.emoji-1F468-1F469-1F466-1F466 { background-position: -440px -540px; }
+.emoji-1F468-1F469-1F467 { background-position: -460px -540px; }
+.emoji-1F468-1F469-1F467-1F466 { background-position: -480px -540px; }
+.emoji-1F468-1F469-1F467-1F467 { background-position: -500px -540px; }
+.emoji-1F468-2764-1F468 { background-position: -520px -540px; }
+.emoji-1F468-2764-1F48B-1F468 { background-position: -540px -540px; }
+.emoji-1F469 { background-position: -560px 0; }
+.emoji-1F469-1F3FB { background-position: -560px -20px; }
+.emoji-1F469-1F3FC { background-position: -560px -40px; }
+.emoji-1F469-1F3FD { background-position: -560px -60px; }
+.emoji-1F469-1F3FE { background-position: -560px -80px; }
+.emoji-1F469-1F3FF { background-position: -560px -100px; }
+.emoji-1F469-1F469-1F466 { background-position: -560px -120px; }
+.emoji-1F469-1F469-1F466-1F466 { background-position: -560px -140px; }
+.emoji-1F469-1F469-1F467 { background-position: -560px -160px; }
+.emoji-1F469-1F469-1F467-1F466 { background-position: -560px -180px; }
+.emoji-1F469-1F469-1F467-1F467 { background-position: -560px -200px; }
+.emoji-1F469-2764-1F469 { background-position: -560px -220px; }
+.emoji-1F469-2764-1F48B-1F469 { background-position: -560px -240px; }
+.emoji-1F46A { background-position: -560px -260px; }
+.emoji-1F46B { background-position: -560px -280px; }
+.emoji-1F46C { background-position: -560px -300px; }
+.emoji-1F46D { background-position: -560px -320px; }
+.emoji-1F46E { background-position: -560px -340px; }
+.emoji-1F46E-1F3FB { background-position: -560px -360px; }
+.emoji-1F46E-1F3FC { background-position: -560px -380px; }
+.emoji-1F46E-1F3FD { background-position: -560px -400px; }
+.emoji-1F46E-1F3FE { background-position: -560px -420px; }
+.emoji-1F46E-1F3FF { background-position: -560px -440px; }
+.emoji-1F46F { background-position: -560px -460px; }
+.emoji-1F470 { background-position: -560px -480px; }
+.emoji-1F470-1F3FB { background-position: -560px -500px; }
+.emoji-1F470-1F3FC { background-position: -560px -520px; }
+.emoji-1F470-1F3FD { background-position: -560px -540px; }
+.emoji-1F470-1F3FE { background-position: 0 -560px; }
+.emoji-1F470-1F3FF { background-position: -20px -560px; }
+.emoji-1F471 { background-position: -40px -560px; }
+.emoji-1F471-1F3FB { background-position: -60px -560px; }
+.emoji-1F471-1F3FC { background-position: -80px -560px; }
+.emoji-1F471-1F3FD { background-position: -100px -560px; }
+.emoji-1F471-1F3FE { background-position: -120px -560px; }
+.emoji-1F471-1F3FF { background-position: -140px -560px; }
+.emoji-1F472 { background-position: -160px -560px; }
+.emoji-1F472-1F3FB { background-position: -180px -560px; }
+.emoji-1F472-1F3FC { background-position: -200px -560px; }
+.emoji-1F472-1F3FD { background-position: -220px -560px; }
+.emoji-1F472-1F3FE { background-position: -240px -560px; }
+.emoji-1F472-1F3FF { background-position: -260px -560px; }
+.emoji-1F473 { background-position: -280px -560px; }
+.emoji-1F473-1F3FB { background-position: -300px -560px; }
+.emoji-1F473-1F3FC { background-position: -320px -560px; }
+.emoji-1F473-1F3FD { background-position: -340px -560px; }
+.emoji-1F473-1F3FE { background-position: -360px -560px; }
+.emoji-1F473-1F3FF { background-position: -380px -560px; }
+.emoji-1F474 { background-position: -400px -560px; }
+.emoji-1F474-1F3FB { background-position: -420px -560px; }
+.emoji-1F474-1F3FC { background-position: -440px -560px; }
+.emoji-1F474-1F3FD { background-position: -460px -560px; }
+.emoji-1F474-1F3FE { background-position: -480px -560px; }
+.emoji-1F474-1F3FF { background-position: -500px -560px; }
+.emoji-1F475 { background-position: -520px -560px; }
+.emoji-1F475-1F3FB { background-position: -540px -560px; }
+.emoji-1F475-1F3FC { background-position: -560px -560px; }
+.emoji-1F475-1F3FD { background-position: -580px 0; }
+.emoji-1F475-1F3FE { background-position: -580px -20px; }
+.emoji-1F475-1F3FF { background-position: -580px -40px; }
+.emoji-1F476 { background-position: -580px -60px; }
+.emoji-1F476-1F3FB { background-position: -580px -80px; }
+.emoji-1F476-1F3FC { background-position: -580px -100px; }
+.emoji-1F476-1F3FD { background-position: -580px -120px; }
+.emoji-1F476-1F3FE { background-position: -580px -140px; }
+.emoji-1F476-1F3FF { background-position: -580px -160px; }
+.emoji-1F477 { background-position: -580px -180px; }
+.emoji-1F477-1F3FB { background-position: -580px -200px; }
+.emoji-1F477-1F3FC { background-position: -580px -220px; }
+.emoji-1F477-1F3FD { background-position: -580px -240px; }
+.emoji-1F477-1F3FE { background-position: -580px -260px; }
+.emoji-1F477-1F3FF { background-position: -580px -280px; }
+.emoji-1F478 { background-position: -580px -300px; }
+.emoji-1F478-1F3FB { background-position: -580px -320px; }
+.emoji-1F478-1F3FC { background-position: -580px -340px; }
+.emoji-1F478-1F3FD { background-position: -580px -360px; }
+.emoji-1F478-1F3FE { background-position: -580px -380px; }
+.emoji-1F478-1F3FF { background-position: -580px -400px; }
+.emoji-1F479 { background-position: -580px -420px; }
+.emoji-1F47A { background-position: -580px -440px; }
+.emoji-1F47B { background-position: -580px -460px; }
+.emoji-1F47C { background-position: -580px -480px; }
+.emoji-1F47C-1F3FB { background-position: -580px -500px; }
+.emoji-1F47C-1F3FC { background-position: -580px -520px; }
+.emoji-1F47C-1F3FD { background-position: -580px -540px; }
+.emoji-1F47C-1F3FE { background-position: -580px -560px; }
+.emoji-1F47C-1F3FF { background-position: 0 -580px; }
+.emoji-1F47D { background-position: -20px -580px; }
+.emoji-1F47E { background-position: -40px -580px; }
+.emoji-1F47F { background-position: -60px -580px; }
+.emoji-1F480 { background-position: -80px -580px; }
+.emoji-1F481 { background-position: -100px -580px; }
+.emoji-1F481-1F3FB { background-position: -120px -580px; }
+.emoji-1F481-1F3FC { background-position: -140px -580px; }
+.emoji-1F481-1F3FD { background-position: -160px -580px; }
+.emoji-1F481-1F3FE { background-position: -180px -580px; }
+.emoji-1F481-1F3FF { background-position: -200px -580px; }
+.emoji-1F482 { background-position: -220px -580px; }
+.emoji-1F482-1F3FB { background-position: -240px -580px; }
+.emoji-1F482-1F3FC { background-position: -260px -580px; }
+.emoji-1F482-1F3FD { background-position: -280px -580px; }
+.emoji-1F482-1F3FE { background-position: -300px -580px; }
+.emoji-1F482-1F3FF { background-position: -320px -580px; }
+.emoji-1F483 { background-position: -340px -580px; }
+.emoji-1F483-1F3FB { background-position: -360px -580px; }
+.emoji-1F483-1F3FC { background-position: -380px -580px; }
+.emoji-1F483-1F3FD { background-position: -400px -580px; }
+.emoji-1F483-1F3FE { background-position: -420px -580px; }
+.emoji-1F483-1F3FF { background-position: -440px -580px; }
+.emoji-1F484 { background-position: -460px -580px; }
+.emoji-1F485 { background-position: -480px -580px; }
+.emoji-1F485-1F3FB { background-position: -500px -580px; }
+.emoji-1F485-1F3FC { background-position: -520px -580px; }
+.emoji-1F485-1F3FD { background-position: -540px -580px; }
+.emoji-1F485-1F3FE { background-position: -560px -580px; }
+.emoji-1F485-1F3FF { background-position: -580px -580px; }
+.emoji-1F486 { background-position: -600px 0; }
+.emoji-1F486-1F3FB { background-position: -600px -20px; }
+.emoji-1F486-1F3FC { background-position: -600px -40px; }
+.emoji-1F486-1F3FD { background-position: -600px -60px; }
+.emoji-1F486-1F3FE { background-position: -600px -80px; }
+.emoji-1F486-1F3FF { background-position: -600px -100px; }
+.emoji-1F487 { background-position: -600px -120px; }
+.emoji-1F487-1F3FB { background-position: -600px -140px; }
+.emoji-1F487-1F3FC { background-position: -600px -160px; }
+.emoji-1F487-1F3FD { background-position: -600px -180px; }
+.emoji-1F487-1F3FE { background-position: -600px -200px; }
+.emoji-1F487-1F3FF { background-position: -600px -220px; }
+.emoji-1F488 { background-position: -600px -240px; }
+.emoji-1F489 { background-position: -600px -260px; }
+.emoji-1F48A { background-position: -600px -280px; }
+.emoji-1F48B { background-position: -600px -300px; }
+.emoji-1F48C { background-position: -600px -320px; }
+.emoji-1F48D { background-position: -600px -340px; }
+.emoji-1F48E { background-position: -600px -360px; }
+.emoji-1F48F { background-position: -600px -380px; }
+.emoji-1F490 { background-position: -600px -400px; }
+.emoji-1F491 { background-position: -600px -420px; }
+.emoji-1F492 { background-position: -600px -440px; }
+.emoji-1F493 { background-position: -600px -460px; }
+.emoji-1F494 { background-position: -600px -480px; }
+.emoji-1F495 { background-position: -600px -500px; }
+.emoji-1F496 { background-position: -600px -520px; }
+.emoji-1F497 { background-position: -600px -540px; }
+.emoji-1F498 { background-position: -600px -560px; }
+.emoji-1F499 { background-position: -600px -580px; }
+.emoji-1F49A { background-position: 0 -600px; }
+.emoji-1F49B { background-position: -20px -600px; }
+.emoji-1F49C { background-position: -40px -600px; }
+.emoji-1F49D { background-position: -60px -600px; }
+.emoji-1F49E { background-position: -80px -600px; }
+.emoji-1F49F { background-position: -100px -600px; }
+.emoji-1F4A0 { background-position: -120px -600px; }
+.emoji-1F4A1 { background-position: -140px -600px; }
+.emoji-1F4A2 { background-position: -160px -600px; }
+.emoji-1F4A3 { background-position: -180px -600px; }
+.emoji-1F4A4 { background-position: -200px -600px; }
+.emoji-1F4A5 { background-position: -220px -600px; }
+.emoji-1F4A6 { background-position: -240px -600px; }
+.emoji-1F4A7 { background-position: -260px -600px; }
+.emoji-1F4A8 { background-position: -280px -600px; }
+.emoji-1F4A9 { background-position: -300px -600px; }
+.emoji-1F4AA { background-position: -320px -600px; }
+.emoji-1F4AA-1F3FB { background-position: -340px -600px; }
+.emoji-1F4AA-1F3FC { background-position: -360px -600px; }
+.emoji-1F4AA-1F3FD { background-position: -380px -600px; }
+.emoji-1F4AA-1F3FE { background-position: -400px -600px; }
+.emoji-1F4AA-1F3FF { background-position: -420px -600px; }
+.emoji-1F4AB { background-position: -440px -600px; }
+.emoji-1F4AC { background-position: -460px -600px; }
+.emoji-1F4AD { background-position: -480px -600px; }
+.emoji-1F4AE { background-position: -500px -600px; }
+.emoji-1F4AF { background-position: -520px -600px; }
+.emoji-1F4B0 { background-position: -540px -600px; }
+.emoji-1F4B1 { background-position: -560px -600px; }
+.emoji-1F4B2 { background-position: -580px -600px; }
+.emoji-1F4B3 { background-position: -600px -600px; }
+.emoji-1F4B4 { background-position: -620px 0; }
+.emoji-1F4B5 { background-position: -620px -20px; }
+.emoji-1F4B6 { background-position: -620px -40px; }
+.emoji-1F4B7 { background-position: -620px -60px; }
+.emoji-1F4B8 { background-position: -620px -80px; }
+.emoji-1F4B9 { background-position: -620px -100px; }
+.emoji-1F4BA { background-position: -620px -120px; }
+.emoji-1F4BB { background-position: -620px -140px; }
+.emoji-1F4BC { background-position: -620px -160px; }
+.emoji-1F4BD { background-position: -620px -180px; }
+.emoji-1F4BE { background-position: -620px -200px; }
+.emoji-1F4BF { background-position: -620px -220px; }
+.emoji-1F4C0 { background-position: -620px -240px; }
+.emoji-1F4C1 { background-position: -620px -260px; }
+.emoji-1F4C2 { background-position: -620px -280px; }
+.emoji-1F4C3 { background-position: -620px -300px; }
+.emoji-1F4C4 { background-position: -620px -320px; }
+.emoji-1F4C5 { background-position: -620px -340px; }
+.emoji-1F4C6 { background-position: -620px -360px; }
+.emoji-1F4C7 { background-position: -620px -380px; }
+.emoji-1F4C8 { background-position: -620px -400px; }
+.emoji-1F4C9 { background-position: -620px -420px; }
+.emoji-1F4CA { background-position: -620px -440px; }
+.emoji-1F4CB { background-position: -620px -460px; }
+.emoji-1F4CC { background-position: -620px -480px; }
+.emoji-1F4CD { background-position: -620px -500px; }
+.emoji-1F4CE { background-position: -620px -520px; }
+.emoji-1F4CF { background-position: -620px -540px; }
+.emoji-1F4D0 { background-position: -620px -560px; }
+.emoji-1F4D1 { background-position: -620px -580px; }
+.emoji-1F4D2 { background-position: -620px -600px; }
+.emoji-1F4D3 { background-position: 0 -620px; }
+.emoji-1F4D4 { background-position: -20px -620px; }
+.emoji-1F4D5 { background-position: -40px -620px; }
+.emoji-1F4D6 { background-position: -60px -620px; }
+.emoji-1F4D7 { background-position: -80px -620px; }
+.emoji-1F4D8 { background-position: -100px -620px; }
+.emoji-1F4D9 { background-position: -120px -620px; }
+.emoji-1F4DA { background-position: -140px -620px; }
+.emoji-1F4DB { background-position: -160px -620px; }
+.emoji-1F4DC { background-position: -180px -620px; }
+.emoji-1F4DD { background-position: -200px -620px; }
+.emoji-1F4DE { background-position: -220px -620px; }
+.emoji-1F4DF { background-position: -240px -620px; }
+.emoji-1F4E0 { background-position: -260px -620px; }
+.emoji-1F4E1 { background-position: -280px -620px; }
+.emoji-1F4E2 { background-position: -300px -620px; }
+.emoji-1F4E3 { background-position: -320px -620px; }
+.emoji-1F4E4 { background-position: -340px -620px; }
+.emoji-1F4E5 { background-position: -360px -620px; }
+.emoji-1F4E6 { background-position: -380px -620px; }
+.emoji-1F4E7 { background-position: -400px -620px; }
+.emoji-1F4E8 { background-position: -420px -620px; }
+.emoji-1F4E9 { background-position: -440px -620px; }
+.emoji-1F4EA { background-position: -460px -620px; }
+.emoji-1F4EB { background-position: -480px -620px; }
+.emoji-1F4EC { background-position: -500px -620px; }
+.emoji-1F4ED { background-position: -520px -620px; }
+.emoji-1F4EE { background-position: -540px -620px; }
+.emoji-1F4EF { background-position: -560px -620px; }
+.emoji-1F4F0 { background-position: -580px -620px; }
+.emoji-1F4F1 { background-position: -600px -620px; }
+.emoji-1F4F2 { background-position: -620px -620px; }
+.emoji-1F4F3 { background-position: -640px 0; }
+.emoji-1F4F4 { background-position: -640px -20px; }
+.emoji-1F4F5 { background-position: -640px -40px; }
+.emoji-1F4F6 { background-position: -640px -60px; }
+.emoji-1F4F7 { background-position: -640px -80px; }
+.emoji-1F4F8 { background-position: -640px -100px; }
+.emoji-1F4F9 { background-position: -640px -120px; }
+.emoji-1F4FA { background-position: -640px -140px; }
+.emoji-1F4FB { background-position: -640px -160px; }
+.emoji-1F4FC { background-position: -640px -180px; }
+.emoji-1F4FD { background-position: -640px -200px; }
+.emoji-1F4FE { background-position: -640px -220px; }
+.emoji-1F4FF { background-position: -640px -240px; }
+.emoji-1F500 { background-position: -640px -260px; }
+.emoji-1F501 { background-position: -640px -280px; }
+.emoji-1F502 { background-position: -640px -300px; }
+.emoji-1F503 { background-position: -640px -320px; }
+.emoji-1F504 { background-position: -640px -340px; }
+.emoji-1F505 { background-position: -640px -360px; }
+.emoji-1F506 { background-position: -640px -380px; }
+.emoji-1F507 { background-position: -640px -400px; }
+.emoji-1F508 { background-position: -640px -420px; }
+.emoji-1F509 { background-position: -640px -440px; }
+.emoji-1F50A { background-position: -640px -460px; }
+.emoji-1F50B { background-position: -640px -480px; }
+.emoji-1F50C { background-position: -640px -500px; }
+.emoji-1F50D { background-position: -640px -520px; }
+.emoji-1F50E { background-position: -640px -540px; }
+.emoji-1F50F { background-position: -640px -560px; }
+.emoji-1F510 { background-position: -640px -580px; }
+.emoji-1F511 { background-position: -640px -600px; }
+.emoji-1F512 { background-position: -640px -620px; }
+.emoji-1F513 { background-position: 0 -640px; }
+.emoji-1F514 { background-position: -20px -640px; }
+.emoji-1F515 { background-position: -40px -640px; }
+.emoji-1F516 { background-position: -60px -640px; }
+.emoji-1F517 { background-position: -80px -640px; }
+.emoji-1F518 { background-position: -100px -640px; }
+.emoji-1F519 { background-position: -120px -640px; }
+.emoji-1F51A { background-position: -140px -640px; }
+.emoji-1F51B { background-position: -160px -640px; }
+.emoji-1F51C { background-position: -180px -640px; }
+.emoji-1F51D { background-position: -200px -640px; }
+.emoji-1F51E { background-position: -220px -640px; }
+.emoji-1F51F { background-position: -240px -640px; }
+.emoji-1F520 { background-position: -260px -640px; }
+.emoji-1F521 { background-position: -280px -640px; }
+.emoji-1F522 { background-position: -300px -640px; }
+.emoji-1F523 { background-position: -320px -640px; }
+.emoji-1F524 { background-position: -340px -640px; }
+.emoji-1F525 { background-position: -360px -640px; }
+.emoji-1F526 { background-position: -380px -640px; }
+.emoji-1F527 { background-position: -400px -640px; }
+.emoji-1F528 { background-position: -420px -640px; }
+.emoji-1F529 { background-position: -440px -640px; }
+.emoji-1F52A { background-position: -460px -640px; }
+.emoji-1F52B { background-position: -480px -640px; }
+.emoji-1F52C { background-position: -500px -640px; }
+.emoji-1F52D { background-position: -520px -640px; }
+.emoji-1F52E { background-position: -540px -640px; }
+.emoji-1F52F { background-position: -560px -640px; }
+.emoji-1F530 { background-position: -580px -640px; }
+.emoji-1F531 { background-position: -600px -640px; }
+.emoji-1F532 { background-position: -620px -640px; }
+.emoji-1F533 { background-position: -640px -640px; }
+.emoji-1F534 { background-position: -660px 0; }
+.emoji-1F535 { background-position: -660px -20px; }
+.emoji-1F536 { background-position: -660px -40px; }
+.emoji-1F537 { background-position: -660px -60px; }
+.emoji-1F538 { background-position: -660px -80px; }
+.emoji-1F539 { background-position: -660px -100px; }
+.emoji-1F53A { background-position: -660px -120px; }
+.emoji-1F53B { background-position: -660px -140px; }
+.emoji-1F53C { background-position: -660px -160px; }
+.emoji-1F53D { background-position: -660px -180px; }
+.emoji-1F546 { background-position: -660px -200px; }
+.emoji-1F547 { background-position: -660px -220px; }
+.emoji-1F548 { background-position: -660px -240px; }
+.emoji-1F549 { background-position: -660px -260px; }
+.emoji-1F54A { background-position: -660px -280px; }
+.emoji-1F54B { background-position: -660px -300px; }
+.emoji-1F54C { background-position: -660px -320px; }
+.emoji-1F54D { background-position: -660px -340px; }
+.emoji-1F54E { background-position: -660px -360px; }
+.emoji-1F550 { background-position: -660px -380px; }
+.emoji-1F551 { background-position: -660px -400px; }
+.emoji-1F552 { background-position: -660px -420px; }
+.emoji-1F553 { background-position: -660px -440px; }
+.emoji-1F554 { background-position: -660px -460px; }
+.emoji-1F555 { background-position: -660px -480px; }
+.emoji-1F556 { background-position: -660px -500px; }
+.emoji-1F557 { background-position: -660px -520px; }
+.emoji-1F558 { background-position: -660px -540px; }
+.emoji-1F559 { background-position: -660px -560px; }
+.emoji-1F55A { background-position: -660px -580px; }
+.emoji-1F55B { background-position: -660px -600px; }
+.emoji-1F55C { background-position: -660px -620px; }
+.emoji-1F55D { background-position: -660px -640px; }
+.emoji-1F55E { background-position: 0 -660px; }
+.emoji-1F55F { background-position: -20px -660px; }
+.emoji-1F560 { background-position: -40px -660px; }
+.emoji-1F561 { background-position: -60px -660px; }
+.emoji-1F562 { background-position: -80px -660px; }
+.emoji-1F563 { background-position: -100px -660px; }
+.emoji-1F564 { background-position: -120px -660px; }
+.emoji-1F565 { background-position: -140px -660px; }
+.emoji-1F566 { background-position: -160px -660px; }
+.emoji-1F567 { background-position: -180px -660px; }
+.emoji-1F568 { background-position: -200px -660px; }
+.emoji-1F569 { background-position: -220px -660px; }
+.emoji-1F56A { background-position: -240px -660px; }
+.emoji-1F56B { background-position: -260px -660px; }
+.emoji-1F56C { background-position: -280px -660px; }
+.emoji-1F56D { background-position: -300px -660px; }
+.emoji-1F56E { background-position: -320px -660px; }
+.emoji-1F56F { background-position: -340px -660px; }
+.emoji-1F570 { background-position: -360px -660px; }
+.emoji-1F571 { background-position: -380px -660px; }
+.emoji-1F572 { background-position: -400px -660px; }
+.emoji-1F573 { background-position: -420px -660px; }
+.emoji-1F574 { background-position: -440px -660px; }
+.emoji-1F575 { background-position: -460px -660px; }
+.emoji-1F575-1F3FB { background-position: -480px -660px; }
+.emoji-1F575-1F3FC { background-position: -500px -660px; }
+.emoji-1F575-1F3FD { background-position: -520px -660px; }
+.emoji-1F575-1F3FE { background-position: -540px -660px; }
+.emoji-1F575-1F3FF { background-position: -560px -660px; }
+.emoji-1F576 { background-position: -580px -660px; }
+.emoji-1F577 { background-position: -600px -660px; }
+.emoji-1F578 { background-position: -620px -660px; }
+.emoji-1F579 { background-position: -640px -660px; }
+.emoji-1F57B { background-position: -660px -660px; }
+.emoji-1F57E { background-position: -680px 0; }
+.emoji-1F57F { background-position: -680px -20px; }
+.emoji-1F581 { background-position: -680px -40px; }
+.emoji-1F582 { background-position: -680px -60px; }
+.emoji-1F583 { background-position: -680px -80px; }
+.emoji-1F585 { background-position: -680px -100px; }
+.emoji-1F586 { background-position: -680px -120px; }
+.emoji-1F587 { background-position: -680px -140px; }
+.emoji-1F588 { background-position: -680px -160px; }
+.emoji-1F589 { background-position: -680px -180px; }
+.emoji-1F58A { background-position: -680px -200px; }
+.emoji-1F58B { background-position: -680px -220px; }
+.emoji-1F58C { background-position: -680px -240px; }
+.emoji-1F58D { background-position: -680px -260px; }
+.emoji-1F58E { background-position: -680px -280px; }
+.emoji-1F58F { background-position: -680px -300px; }
+.emoji-1F590 { background-position: -680px -320px; }
+.emoji-1F590-1F3FB { background-position: -680px -340px; }
+.emoji-1F590-1F3FC { background-position: -680px -360px; }
+.emoji-1F590-1F3FD { background-position: -680px -380px; }
+.emoji-1F590-1F3FE { background-position: -680px -400px; }
+.emoji-1F590-1F3FF { background-position: -680px -420px; }
+.emoji-1F591 { background-position: -680px -440px; }
+.emoji-1F592 { background-position: -680px -460px; }
+.emoji-1F593 { background-position: -680px -480px; }
+.emoji-1F594 { background-position: -680px -500px; }
+.emoji-1F595 { background-position: -680px -520px; }
+.emoji-1F595-1F3FB { background-position: -680px -540px; }
+.emoji-1F595-1F3FC { background-position: -680px -560px; }
+.emoji-1F595-1F3FD { background-position: -680px -580px; }
+.emoji-1F595-1F3FE { background-position: -680px -600px; }
+.emoji-1F595-1F3FF { background-position: -680px -620px; }
+.emoji-1F596 { background-position: -680px -640px; }
+.emoji-1F596-1F3FB { background-position: -680px -660px; }
+.emoji-1F596-1F3FC { background-position: 0 -680px; }
+.emoji-1F596-1F3FD { background-position: -20px -680px; }
+.emoji-1F596-1F3FE { background-position: -40px -680px; }
+.emoji-1F596-1F3FF { background-position: -60px -680px; }
+.emoji-1F597 { background-position: -80px -680px; }
+.emoji-1F598 { background-position: -100px -680px; }
+.emoji-1F599 { background-position: -120px -680px; }
+.emoji-1F59E { background-position: -140px -680px; }
+.emoji-1F59F { background-position: -160px -680px; }
+.emoji-1F5A5 { background-position: -180px -680px; }
+.emoji-1F5A6 { background-position: -200px -680px; }
+.emoji-1F5A7 { background-position: -220px -680px; }
+.emoji-1F5A8 { background-position: -240px -680px; }
+.emoji-1F5A9 { background-position: -260px -680px; }
+.emoji-1F5AA { background-position: -280px -680px; }
+.emoji-1F5AB { background-position: -300px -680px; }
+.emoji-1F5AD { background-position: -320px -680px; }
+.emoji-1F5AE { background-position: -340px -680px; }
+.emoji-1F5AF { background-position: -360px -680px; }
+.emoji-1F5B1 { background-position: -380px -680px; }
+.emoji-1F5B2 { background-position: -400px -680px; }
+.emoji-1F5B3 { background-position: -420px -680px; }
+.emoji-1F5B4 { background-position: -440px -680px; }
+.emoji-1F5B8 { background-position: -460px -680px; }
+.emoji-1F5B9 { background-position: -480px -680px; }
+.emoji-1F5BC { background-position: -500px -680px; }
+.emoji-1F5BD { background-position: -520px -680px; }
+.emoji-1F5BE { background-position: -540px -680px; }
+.emoji-1F5C0 { background-position: -560px -680px; }
+.emoji-1F5C1 { background-position: -580px -680px; }
+.emoji-1F5C2 { background-position: -600px -680px; }
+.emoji-1F5C3 { background-position: -620px -680px; }
+.emoji-1F5C4 { background-position: -640px -680px; }
+.emoji-1F5C6 { background-position: -660px -680px; }
+.emoji-1F5C7 { background-position: -680px -680px; }
+.emoji-1F5C9 { background-position: -700px 0; }
+.emoji-1F5CA { background-position: -700px -20px; }
+.emoji-1F5CE { background-position: -700px -40px; }
+.emoji-1F5CF { background-position: -700px -60px; }
+.emoji-1F5D0 { background-position: -700px -80px; }
+.emoji-1F5D1 { background-position: -700px -100px; }
+.emoji-1F5D2 { background-position: -700px -120px; }
+.emoji-1F5D3 { background-position: -700px -140px; }
+.emoji-1F5D4 { background-position: -700px -160px; }
+.emoji-1F5D8 { background-position: -700px -180px; }
+.emoji-1F5D9 { background-position: -700px -200px; }
+.emoji-1F5DC { background-position: -700px -220px; }
+.emoji-1F5DD { background-position: -700px -240px; }
+.emoji-1F5DE { background-position: -700px -260px; }
+.emoji-1F5E0 { background-position: -700px -280px; }
+.emoji-1F5E1 { background-position: -700px -300px; }
+.emoji-1F5E2 { background-position: -700px -320px; }
+.emoji-1F5E3 { background-position: -700px -340px; }
+.emoji-1F5E8 { background-position: -700px -360px; }
+.emoji-1F5E9 { background-position: -700px -380px; }
+.emoji-1F5EA { background-position: -700px -400px; }
+.emoji-1F5EB { background-position: -700px -420px; }
+.emoji-1F5EC { background-position: -700px -440px; }
+.emoji-1F5ED { background-position: -700px -460px; }
+.emoji-1F5EE { background-position: -700px -480px; }
+.emoji-1F5EF { background-position: -700px -500px; }
+.emoji-1F5F0 { background-position: -700px -520px; }
+.emoji-1F5F1 { background-position: -700px -540px; }
+.emoji-1F5F2 { background-position: -700px -560px; }
+.emoji-1F5F3 { background-position: -700px -580px; }
+.emoji-1F5F4 { background-position: -700px -600px; }
+.emoji-1F5F5 { background-position: -700px -620px; }
+.emoji-1F5F8 { background-position: -700px -640px; }
+.emoji-1F5F9 { background-position: -700px -660px; }
+.emoji-1F5FA { background-position: -700px -680px; }
+.emoji-1F5FB { background-position: 0 -700px; }
+.emoji-1F5FC { background-position: -20px -700px; }
+.emoji-1F5FD { background-position: -40px -700px; }
+.emoji-1F5FE { background-position: -60px -700px; }
+.emoji-1F5FF { background-position: -80px -700px; }
+.emoji-1F600 { background-position: -100px -700px; }
+.emoji-1F601 { background-position: -120px -700px; }
+.emoji-1F602 { background-position: -140px -700px; }
+.emoji-1F603 { background-position: -160px -700px; }
+.emoji-1F604 { background-position: -180px -700px; }
+.emoji-1F605 { background-position: -200px -700px; }
+.emoji-1F606 { background-position: -220px -700px; }
+.emoji-1F607 { background-position: -240px -700px; }
+.emoji-1F608 { background-position: -260px -700px; }
+.emoji-1F609 { background-position: -280px -700px; }
+.emoji-1F60A { background-position: -300px -700px; }
+.emoji-1F60B { background-position: -320px -700px; }
+.emoji-1F60C { background-position: -340px -700px; }
+.emoji-1F60D { background-position: -360px -700px; }
+.emoji-1F60E { background-position: -380px -700px; }
+.emoji-1F60F { background-position: -400px -700px; }
+.emoji-1F610 { background-position: -420px -700px; }
+.emoji-1F611 { background-position: -440px -700px; }
+.emoji-1F612 { background-position: -460px -700px; }
+.emoji-1F613 { background-position: -480px -700px; }
+.emoji-1F614 { background-position: -500px -700px; }
+.emoji-1F615 { background-position: -520px -700px; }
+.emoji-1F616 { background-position: -540px -700px; }
+.emoji-1F617 { background-position: -560px -700px; }
+.emoji-1F618 { background-position: -580px -700px; }
+.emoji-1F619 { background-position: -600px -700px; }
+.emoji-1F61A { background-position: -620px -700px; }
+.emoji-1F61B { background-position: -640px -700px; }
+.emoji-1F61C { background-position: -660px -700px; }
+.emoji-1F61D { background-position: -680px -700px; }
+.emoji-1F61E { background-position: -700px -700px; }
+.emoji-1F61F { background-position: -720px 0; }
+.emoji-1F620 { background-position: -720px -20px; }
+.emoji-1F621 { background-position: -720px -40px; }
+.emoji-1F622 { background-position: -720px -60px; }
+.emoji-1F623 { background-position: -720px -80px; }
+.emoji-1F624 { background-position: -720px -100px; }
+.emoji-1F625 { background-position: -720px -120px; }
+.emoji-1F626 { background-position: -720px -140px; }
+.emoji-1F627 { background-position: -720px -160px; }
+.emoji-1F628 { background-position: -720px -180px; }
+.emoji-1F629 { background-position: -720px -200px; }
+.emoji-1F62A { background-position: -720px -220px; }
+.emoji-1F62B { background-position: -720px -240px; }
+.emoji-1F62C { background-position: -720px -260px; }
+.emoji-1F62D { background-position: -720px -280px; }
+.emoji-1F62E { background-position: -720px -300px; }
+.emoji-1F62F { background-position: -720px -320px; }
+.emoji-1F630 { background-position: -720px -340px; }
+.emoji-1F631 { background-position: -720px -360px; }
+.emoji-1F632 { background-position: -720px -380px; }
+.emoji-1F633 { background-position: -720px -400px; }
+.emoji-1F634 { background-position: -720px -420px; }
+.emoji-1F635 { background-position: -720px -440px; }
+.emoji-1F636 { background-position: -720px -460px; }
+.emoji-1F637 { background-position: -720px -480px; }
+.emoji-1F638 { background-position: -720px -500px; }
+.emoji-1F639 { background-position: -720px -520px; }
+.emoji-1F63A { background-position: -720px -540px; }
+.emoji-1F63B { background-position: -720px -560px; }
+.emoji-1F63C { background-position: -720px -580px; }
+.emoji-1F63D { background-position: -720px -600px; }
+.emoji-1F63E { background-position: -720px -620px; }
+.emoji-1F63F { background-position: -720px -640px; }
+.emoji-1F640 { background-position: -720px -660px; }
+.emoji-1F641 { background-position: -720px -680px; }
+.emoji-1F642 { background-position: -720px -700px; }
+.emoji-1F643 { background-position: 0 -720px; }
+.emoji-1F644 { background-position: -20px -720px; }
+.emoji-1F645 { background-position: -40px -720px; }
+.emoji-1F645-1F3FB { background-position: -60px -720px; }
+.emoji-1F645-1F3FC { background-position: -80px -720px; }
+.emoji-1F645-1F3FD { background-position: -100px -720px; }
+.emoji-1F645-1F3FE { background-position: -120px -720px; }
+.emoji-1F645-1F3FF { background-position: -140px -720px; }
+.emoji-1F646 { background-position: -160px -720px; }
+.emoji-1F646-1F3FB { background-position: -180px -720px; }
+.emoji-1F646-1F3FC { background-position: -200px -720px; }
+.emoji-1F646-1F3FD { background-position: -220px -720px; }
+.emoji-1F646-1F3FE { background-position: -240px -720px; }
+.emoji-1F646-1F3FF { background-position: -260px -720px; }
+.emoji-1F647 { background-position: -280px -720px; }
+.emoji-1F647-1F3FB { background-position: -300px -720px; }
+.emoji-1F647-1F3FC { background-position: -320px -720px; }
+.emoji-1F647-1F3FD { background-position: -340px -720px; }
+.emoji-1F647-1F3FE { background-position: -360px -720px; }
+.emoji-1F647-1F3FF { background-position: -380px -720px; }
+.emoji-1F648 { background-position: -400px -720px; }
+.emoji-1F649 { background-position: -420px -720px; }
+.emoji-1F64A { background-position: -440px -720px; }
+.emoji-1F64B { background-position: -460px -720px; }
+.emoji-1F64B-1F3FB { background-position: -480px -720px; }
+.emoji-1F64B-1F3FC { background-position: -500px -720px; }
+.emoji-1F64B-1F3FD { background-position: -520px -720px; }
+.emoji-1F64B-1F3FE { background-position: -540px -720px; }
+.emoji-1F64B-1F3FF { background-position: -560px -720px; }
+.emoji-1F64C { background-position: -580px -720px; }
+.emoji-1F64C-1F3FB { background-position: -600px -720px; }
+.emoji-1F64C-1F3FC { background-position: -620px -720px; }
+.emoji-1F64C-1F3FD { background-position: -640px -720px; }
+.emoji-1F64C-1F3FE { background-position: -660px -720px; }
+.emoji-1F64C-1F3FF { background-position: -680px -720px; }
+.emoji-1F64D { background-position: -700px -720px; }
+.emoji-1F64D-1F3FB { background-position: -720px -720px; }
+.emoji-1F64D-1F3FC { background-position: -740px 0; }
+.emoji-1F64D-1F3FD { background-position: -740px -20px; }
+.emoji-1F64D-1F3FE { background-position: -740px -40px; }
+.emoji-1F64D-1F3FF { background-position: -740px -60px; }
+.emoji-1F64E { background-position: -740px -80px; }
+.emoji-1F64E-1F3FB { background-position: -740px -100px; }
+.emoji-1F64E-1F3FC { background-position: -740px -120px; }
+.emoji-1F64E-1F3FD { background-position: -740px -140px; }
+.emoji-1F64E-1F3FE { background-position: -740px -160px; }
+.emoji-1F64E-1F3FF { background-position: -740px -180px; }
+.emoji-1F64F { background-position: -740px -200px; }
+.emoji-1F64F-1F3FB { background-position: -740px -220px; }
+.emoji-1F64F-1F3FC { background-position: -740px -240px; }
+.emoji-1F64F-1F3FD { background-position: -740px -260px; }
+.emoji-1F64F-1F3FE { background-position: -740px -280px; }
+.emoji-1F64F-1F3FF { background-position: -740px -300px; }
+.emoji-1F680 { background-position: -740px -320px; }
+.emoji-1F681 { background-position: -740px -340px; }
+.emoji-1F682 { background-position: -740px -360px; }
+.emoji-1F683 { background-position: -740px -380px; }
+.emoji-1F684 { background-position: -740px -400px; }
+.emoji-1F685 { background-position: -740px -420px; }
+.emoji-1F686 { background-position: -740px -440px; }
+.emoji-1F687 { background-position: -740px -460px; }
+.emoji-1F688 { background-position: -740px -480px; }
+.emoji-1F689 { background-position: -740px -500px; }
+.emoji-1F68A { background-position: -740px -520px; }
+.emoji-1F68B { background-position: -740px -540px; }
+.emoji-1F68C { background-position: -740px -560px; }
+.emoji-1F68D { background-position: -740px -580px; }
+.emoji-1F68E { background-position: -740px -600px; }
+.emoji-1F68F { background-position: -740px -620px; }
+.emoji-1F690 { background-position: -740px -640px; }
+.emoji-1F691 { background-position: -740px -660px; }
+.emoji-1F692 { background-position: -740px -680px; }
+.emoji-1F693 { background-position: -740px -700px; }
+.emoji-1F694 { background-position: -740px -720px; }
+.emoji-1F695 { background-position: 0 -740px; }
+.emoji-1F696 { background-position: -20px -740px; }
+.emoji-1F697 { background-position: -40px -740px; }
+.emoji-1F698 { background-position: -60px -740px; }
+.emoji-1F699 { background-position: -80px -740px; }
+.emoji-1F69A { background-position: -100px -740px; }
+.emoji-1F69B { background-position: -120px -740px; }
+.emoji-1F69C { background-position: -140px -740px; }
+.emoji-1F69D { background-position: -160px -740px; }
+.emoji-1F69E { background-position: -180px -740px; }
+.emoji-1F69F { background-position: -200px -740px; }
+.emoji-1F6A0 { background-position: -220px -740px; }
+.emoji-1F6A1 { background-position: -240px -740px; }
+.emoji-1F6A2 { background-position: -260px -740px; }
+.emoji-1F6A3 { background-position: -280px -740px; }
+.emoji-1F6A3-1F3FB { background-position: -300px -740px; }
+.emoji-1F6A3-1F3FC { background-position: -320px -740px; }
+.emoji-1F6A3-1F3FD { background-position: -340px -740px; }
+.emoji-1F6A3-1F3FE { background-position: -360px -740px; }
+.emoji-1F6A3-1F3FF { background-position: -380px -740px; }
+.emoji-1F6A4 { background-position: -400px -740px; }
+.emoji-1F6A5 { background-position: -420px -740px; }
+.emoji-1F6A6 { background-position: -440px -740px; }
+.emoji-1F6A7 { background-position: -460px -740px; }
+.emoji-1F6A8 { background-position: -480px -740px; }
+.emoji-1F6A9 { background-position: -500px -740px; }
+.emoji-1F6AA { background-position: -520px -740px; }
+.emoji-1F6AB { background-position: -540px -740px; }
+.emoji-1F6AC { background-position: -560px -740px; }
+.emoji-1F6AD { background-position: -580px -740px; }
+.emoji-1F6AE { background-position: -600px -740px; }
+.emoji-1F6AF { background-position: -620px -740px; }
+.emoji-1F6B0 { background-position: -640px -740px; }
+.emoji-1F6B1 { background-position: -660px -740px; }
+.emoji-1F6B2 { background-position: -680px -740px; }
+.emoji-1F6B3 { background-position: -700px -740px; }
+.emoji-1F6B4 { background-position: -720px -740px; }
+.emoji-1F6B4-1F3FB { background-position: -740px -740px; }
+.emoji-1F6B4-1F3FC { background-position: -760px 0; }
+.emoji-1F6B4-1F3FD { background-position: -760px -20px; }
+.emoji-1F6B4-1F3FE { background-position: -760px -40px; }
+.emoji-1F6B4-1F3FF { background-position: -760px -60px; }
+.emoji-1F6B5 { background-position: -760px -80px; }
+.emoji-1F6B5-1F3FB { background-position: -760px -100px; }
+.emoji-1F6B5-1F3FC { background-position: -760px -120px; }
+.emoji-1F6B5-1F3FD { background-position: -760px -140px; }
+.emoji-1F6B5-1F3FE { background-position: -760px -160px; }
+.emoji-1F6B5-1F3FF { background-position: -760px -180px; }
+.emoji-1F6B6 { background-position: -760px -200px; }
+.emoji-1F6B6-1F3FB { background-position: -760px -220px; }
+.emoji-1F6B6-1F3FC { background-position: -760px -240px; }
+.emoji-1F6B6-1F3FD { background-position: -760px -260px; }
+.emoji-1F6B6-1F3FE { background-position: -760px -280px; }
+.emoji-1F6B6-1F3FF { background-position: -760px -300px; }
+.emoji-1F6B7 { background-position: -760px -320px; }
+.emoji-1F6B8 { background-position: -760px -340px; }
+.emoji-1F6B9 { background-position: -760px -360px; }
+.emoji-1F6BA { background-position: -760px -380px; }
+.emoji-1F6BB { background-position: -760px -400px; }
+.emoji-1F6BC { background-position: -760px -420px; }
+.emoji-1F6BD { background-position: -760px -440px; }
+.emoji-1F6BE { background-position: -760px -460px; }
+.emoji-1F6BF { background-position: -760px -480px; }
+.emoji-1F6C0 { background-position: -760px -500px; }
+.emoji-1F6C0-1F3FB { background-position: -760px -520px; }
+.emoji-1F6C0-1F3FC { background-position: -760px -540px; }
+.emoji-1F6C0-1F3FD { background-position: -760px -560px; }
+.emoji-1F6C0-1F3FE { background-position: -760px -580px; }
+.emoji-1F6C0-1F3FF { background-position: -760px -600px; }
+.emoji-1F6C1 { background-position: -760px -620px; }
+.emoji-1F6C2 { background-position: -760px -640px; }
+.emoji-1F6C3 { background-position: -760px -660px; }
+.emoji-1F6C4 { background-position: -760px -680px; }
+.emoji-1F6C5 { background-position: -760px -700px; }
+.emoji-1F6C6 { background-position: -760px -720px; }
+.emoji-1F6C7 { background-position: -760px -740px; }
+.emoji-1F6C8 { background-position: 0 -760px; }
+.emoji-1F6C9 { background-position: -20px -760px; }
+.emoji-1F6CA { background-position: -40px -760px; }
+.emoji-1F6CB { background-position: -60px -760px; }
+.emoji-1F6CC { background-position: -80px -760px; }
+.emoji-1F6CD { background-position: -100px -760px; }
+.emoji-1F6CE { background-position: -120px -760px; }
+.emoji-1F6CF { background-position: -140px -760px; }
+.emoji-1F6D0 { background-position: -160px -760px; }
+.emoji-1F6E0 { background-position: -180px -760px; }
+.emoji-1F6E1 { background-position: -200px -760px; }
+.emoji-1F6E2 { background-position: -220px -760px; }
+.emoji-1F6E3 { background-position: -240px -760px; }
+.emoji-1F6E4 { background-position: -260px -760px; }
+.emoji-1F6E5 { background-position: -280px -760px; }
+.emoji-1F6E6 { background-position: -300px -760px; }
+.emoji-1F6E7 { background-position: -320px -760px; }
+.emoji-1F6E8 { background-position: -340px -760px; }
+.emoji-1F6E9 { background-position: -360px -760px; }
+.emoji-1F6EA { background-position: -380px -760px; }
+.emoji-1F6EB { background-position: -400px -760px; }
+.emoji-1F6EC { background-position: -420px -760px; }
+.emoji-1F6F0 { background-position: -440px -760px; }
+.emoji-1F6F1 { background-position: -460px -760px; }
+.emoji-1F6F2 { background-position: -480px -760px; }
+.emoji-1F6F3 { background-position: -500px -760px; }
+.emoji-1F910 { background-position: -520px -760px; }
+.emoji-1F911 { background-position: -540px -760px; }
+.emoji-1F912 { background-position: -560px -760px; }
+.emoji-1F913 { background-position: -580px -760px; }
+.emoji-1F914 { background-position: -600px -760px; }
+.emoji-1F915 { background-position: -620px -760px; }
+.emoji-1F916 { background-position: -640px -760px; }
+.emoji-1F917 { background-position: -660px -760px; }
+.emoji-1F918 { background-position: -680px -760px; }
+.emoji-1F918-1F3FB { background-position: -700px -760px; }
+.emoji-1F918-1F3FC { background-position: -720px -760px; }
+.emoji-1F918-1F3FD { background-position: -740px -760px; }
+.emoji-1F918-1F3FE { background-position: -760px -760px; }
+.emoji-1F918-1F3FF { background-position: -780px 0; }
+.emoji-1F980 { background-position: -780px -20px; }
+.emoji-1F981 { background-position: -780px -40px; }
+.emoji-1F982 { background-position: -780px -60px; }
+.emoji-1F983 { background-position: -780px -80px; }
+.emoji-1F984 { background-position: -780px -100px; }
+.emoji-1F9C0 { background-position: -780px -120px; }
+.emoji-203C { background-position: -780px -140px; }
+.emoji-2049 { background-position: -780px -160px; }
+.emoji-2122 { background-position: -780px -180px; }
+.emoji-2139 { background-position: -780px -200px; }
+.emoji-2194 { background-position: -780px -220px; }
+.emoji-2195 { background-position: -780px -240px; }
+.emoji-2196 { background-position: -780px -260px; }
+.emoji-2197 { background-position: -780px -280px; }
+.emoji-2198 { background-position: -780px -300px; }
+.emoji-2199 { background-position: -780px -320px; }
+.emoji-21A9 { background-position: -780px -340px; }
+.emoji-21AA { background-position: -780px -360px; }
+.emoji-231A { background-position: -780px -380px; }
+.emoji-231B { background-position: -780px -400px; }
+.emoji-2328 { background-position: -780px -420px; }
+.emoji-23E9 { background-position: -780px -440px; }
+.emoji-23EA { background-position: -780px -460px; }
+.emoji-23EB { background-position: -780px -480px; }
+.emoji-23EC { background-position: -780px -500px; }
+.emoji-23ED { background-position: -780px -520px; }
+.emoji-23EE { background-position: -780px -540px; }
+.emoji-23EF { background-position: -780px -560px; }
+.emoji-23F0 { background-position: -780px -580px; }
+.emoji-23F1 { background-position: -780px -600px; }
+.emoji-23F2 { background-position: -780px -620px; }
+.emoji-23F3 { background-position: -780px -640px; }
+.emoji-23F8 { background-position: -780px -660px; }
+.emoji-23F9 { background-position: -780px -680px; }
+.emoji-23FA { background-position: -780px -700px; }
+.emoji-24C2 { background-position: -780px -720px; }
+.emoji-25AA { background-position: -780px -740px; }
+.emoji-25AB { background-position: -780px -760px; }
+.emoji-25B6 { background-position: 0 -780px; }
+.emoji-25C0 { background-position: -20px -780px; }
+.emoji-25FB { background-position: -40px -780px; }
+.emoji-25FC { background-position: -60px -780px; }
+.emoji-25FD { background-position: -80px -780px; }
+.emoji-25FE { background-position: -100px -780px; }
+.emoji-2600 { background-position: -120px -780px; }
+.emoji-2601 { background-position: -140px -780px; }
+.emoji-2602 { background-position: -160px -780px; }
+.emoji-2603 { background-position: -180px -780px; }
+.emoji-2604 { background-position: -200px -780px; }
+.emoji-260E { background-position: -220px -780px; }
+.emoji-2611 { background-position: -240px -780px; }
+.emoji-2614 { background-position: -260px -780px; }
+.emoji-2615 { background-position: -280px -780px; }
+.emoji-2618 { background-position: -300px -780px; }
+.emoji-261D { background-position: -320px -780px; }
+.emoji-261D-1F3FB { background-position: -340px -780px; }
+.emoji-261D-1F3FC { background-position: -360px -780px; }
+.emoji-261D-1F3FD { background-position: -380px -780px; }
+.emoji-261D-1F3FE { background-position: -400px -780px; }
+.emoji-261D-1F3FF { background-position: -420px -780px; }
+.emoji-2620 { background-position: -440px -780px; }
+.emoji-2622 { background-position: -460px -780px; }
+.emoji-2623 { background-position: -480px -780px; }
+.emoji-2626 { background-position: -500px -780px; }
+.emoji-262A { background-position: -520px -780px; }
+.emoji-262E { background-position: -540px -780px; }
+.emoji-262F { background-position: -560px -780px; }
+.emoji-2638 { background-position: -580px -780px; }
+.emoji-2639 { background-position: -600px -780px; }
+.emoji-263A { background-position: -620px -780px; }
+.emoji-2648 { background-position: -640px -780px; }
+.emoji-2649 { background-position: -660px -780px; }
+.emoji-264A { background-position: -680px -780px; }
+.emoji-264B { background-position: -700px -780px; }
+.emoji-264C { background-position: -720px -780px; }
+.emoji-264D { background-position: -740px -780px; }
+.emoji-264E { background-position: -760px -780px; }
+.emoji-264F { background-position: -780px -780px; }
+.emoji-2650 { background-position: -800px 0; }
+.emoji-2651 { background-position: -800px -20px; }
+.emoji-2652 { background-position: -800px -40px; }
+.emoji-2653 { background-position: -800px -60px; }
+.emoji-2660 { background-position: -800px -80px; }
+.emoji-2663 { background-position: -800px -100px; }
+.emoji-2665 { background-position: -800px -120px; }
+.emoji-2666 { background-position: -800px -140px; }
+.emoji-2668 { background-position: -800px -160px; }
+.emoji-267B { background-position: -800px -180px; }
+.emoji-267F { background-position: -800px -200px; }
+.emoji-2692 { background-position: -800px -220px; }
+.emoji-2693 { background-position: -800px -240px; }
+.emoji-2694 { background-position: -800px -260px; }
+.emoji-2696 { background-position: -800px -280px; }
+.emoji-2697 { background-position: -800px -300px; }
+.emoji-2699 { background-position: -800px -320px; }
+.emoji-269B { background-position: -800px -340px; }
+.emoji-269C { background-position: -800px -360px; }
+.emoji-26A0 { background-position: -800px -380px; }
+.emoji-26A1 { background-position: -800px -400px; }
+.emoji-26AA { background-position: -800px -420px; }
+.emoji-26AB { background-position: -800px -440px; }
+.emoji-26B0 { background-position: -800px -460px; }
+.emoji-26B1 { background-position: -800px -480px; }
+.emoji-26BD { background-position: -800px -500px; }
+.emoji-26BE { background-position: -800px -520px; }
+.emoji-26C4 { background-position: -800px -540px; }
+.emoji-26C5 { background-position: -800px -560px; }
+.emoji-26C8 { background-position: -800px -580px; }
+.emoji-26CE { background-position: -800px -600px; }
+.emoji-26CF { background-position: -800px -620px; }
+.emoji-26D1 { background-position: -800px -640px; }
+.emoji-26D3 { background-position: -800px -660px; }
+.emoji-26D4 { background-position: -800px -680px; }
+.emoji-26E9 { background-position: -800px -700px; }
+.emoji-26EA { background-position: -800px -720px; }
+.emoji-26F0 { background-position: -800px -740px; }
+.emoji-26F1 { background-position: -800px -760px; }
+.emoji-26F2 { background-position: -800px -780px; }
+.emoji-26F3 { background-position: 0 -800px; }
+.emoji-26F4 { background-position: -20px -800px; }
+.emoji-26F5 { background-position: -40px -800px; }
+.emoji-26F7 { background-position: -60px -800px; }
+.emoji-26F8 { background-position: -80px -800px; }
+.emoji-26F9 { background-position: -100px -800px; }
+.emoji-26F9-1F3FB { background-position: -120px -800px; }
+.emoji-26F9-1F3FC { background-position: -140px -800px; }
+.emoji-26F9-1F3FD { background-position: -160px -800px; }
+.emoji-26F9-1F3FE { background-position: -180px -800px; }
+.emoji-26F9-1F3FF { background-position: -200px -800px; }
+.emoji-26FA { background-position: -220px -800px; }
+.emoji-26FD { background-position: -240px -800px; }
+.emoji-2702 { background-position: -260px -800px; }
+.emoji-2705 { background-position: -280px -800px; }
+.emoji-2708 { background-position: -300px -800px; }
+.emoji-2709 { background-position: -320px -800px; }
+.emoji-270A { background-position: -340px -800px; }
+.emoji-270A-1F3FB { background-position: -360px -800px; }
+.emoji-270A-1F3FC { background-position: -380px -800px; }
+.emoji-270A-1F3FD { background-position: -400px -800px; }
+.emoji-270A-1F3FE { background-position: -420px -800px; }
+.emoji-270A-1F3FF { background-position: -440px -800px; }
+.emoji-270B { background-position: -460px -800px; }
+.emoji-270B-1F3FB { background-position: -480px -800px; }
+.emoji-270B-1F3FC { background-position: -500px -800px; }
+.emoji-270B-1F3FD { background-position: -520px -800px; }
+.emoji-270B-1F3FE { background-position: -540px -800px; }
+.emoji-270B-1F3FF { background-position: -560px -800px; }
+.emoji-270C { background-position: -580px -800px; }
+.emoji-270C-1F3FB { background-position: -600px -800px; }
+.emoji-270C-1F3FC { background-position: -620px -800px; }
+.emoji-270C-1F3FD { background-position: -640px -800px; }
+.emoji-270C-1F3FE { background-position: -660px -800px; }
+.emoji-270C-1F3FF { background-position: -680px -800px; }
+.emoji-270D { background-position: -700px -800px; }
+.emoji-270D-1F3FB { background-position: -720px -800px; }
+.emoji-270D-1F3FC { background-position: -740px -800px; }
+.emoji-270D-1F3FD { background-position: -760px -800px; }
+.emoji-270D-1F3FE { background-position: -780px -800px; }
+.emoji-270D-1F3FF { background-position: -800px -800px; }
+.emoji-270F { background-position: -820px 0; }
+.emoji-2712 { background-position: -820px -20px; }
+.emoji-2714 { background-position: -820px -40px; }
+.emoji-2716 { background-position: -820px -60px; }
+.emoji-271D { background-position: -820px -80px; }
+.emoji-2721 { background-position: -820px -100px; }
+.emoji-2728 { background-position: -820px -120px; }
+.emoji-2733 { background-position: -820px -140px; }
+.emoji-2734 { background-position: -820px -160px; }
+.emoji-2744 { background-position: -820px -180px; }
+.emoji-2747 { background-position: -820px -200px; }
+.emoji-274C { background-position: -820px -220px; }
+.emoji-274E { background-position: -820px -240px; }
+.emoji-2753 { background-position: -820px -260px; }
+.emoji-2754 { background-position: -820px -280px; }
+.emoji-2755 { background-position: -820px -300px; }
+.emoji-2757 { background-position: -820px -320px; }
+.emoji-2763 { background-position: -820px -340px; }
+.emoji-2764 { background-position: -820px -360px; }
+.emoji-2795 { background-position: -820px -380px; }
+.emoji-2796 { background-position: -820px -400px; }
+.emoji-2797 { background-position: -820px -420px; }
+.emoji-27A1 { background-position: -820px -440px; }
+.emoji-27B0 { background-position: -820px -460px; }
+.emoji-27BF { background-position: -820px -480px; }
+.emoji-2934 { background-position: -820px -500px; }
+.emoji-2935 { background-position: -820px -520px; }
+.emoji-2B05 { background-position: -820px -540px; }
+.emoji-2B06 { background-position: -820px -560px; }
+.emoji-2B07 { background-position: -820px -580px; }
+.emoji-2B1B { background-position: -820px -600px; }
+.emoji-2B1C { background-position: -820px -620px; }
+.emoji-2B50 { background-position: -820px -640px; }
+.emoji-2B55 { background-position: -820px -660px; }
+.emoji-3030 { background-position: -820px -680px; }
+.emoji-303D { background-position: -820px -700px; }
+.emoji-3297 { background-position: -820px -720px; }
+.emoji-3299 { background-position: -820px -740px; }
-.emoji-icon{
- background-image: image-url("emoji.png");
+.emoji-icon {
+ background-image: image-url('emoji.png');
background-repeat: no-repeat;
-}
+ height: 20px;
+ width: 20px;
-.emoji-0023-20E3 { background-position: 0px 0px; }
-.emoji-0030-20E3 { background-position: -20px 0px; }
-.emoji-0031-20E3 { background-position: -40px 0px; }
-.emoji-0032-20E3 { background-position: -60px 0px; }
-.emoji-0033-20E3 { background-position: -80px 0px; }
-.emoji-0034-20E3 { background-position: -100px 0px; }
-.emoji-0035-20E3 { background-position: -120px 0px; }
-.emoji-0036-20E3 { background-position: -140px 0px; }
-.emoji-0037-20E3 { background-position: -160px 0px; }
-.emoji-0038-20E3 { background-position: -180px 0px; }
-.emoji-0039-20E3 { background-position: -200px 0px; }
-.emoji-00A9 { background-position: -220px 0px; }
-.emoji-00AE { background-position: -240px 0px; }
-.emoji-1F004 { background-position: -260px 0px; }
-.emoji-1F0CF { background-position: -280px 0px; }
-.emoji-1F170 { background-position: -300px 0px; }
-.emoji-1F171 { background-position: -320px 0px; }
-.emoji-1F17E { background-position: -340px 0px; }
-.emoji-1F17F { background-position: -360px 0px; }
-.emoji-1F18E { background-position: -380px 0px; }
-.emoji-1F191 { background-position: -400px 0px; }
-.emoji-1F192 { background-position: -420px 0px; }
-.emoji-1F193 { background-position: -440px 0px; }
-.emoji-1F194 { background-position: -460px 0px; }
-.emoji-1F195 { background-position: -480px 0px; }
-.emoji-1F196 { background-position: -500px 0px; }
-.emoji-1F197 { background-position: -520px 0px; }
-.emoji-1F198 { background-position: -540px 0px; }
-.emoji-1F199 { background-position: -560px 0px; }
-.emoji-1F19A { background-position: -580px 0px; }
-.emoji-1F1E6-1F1E8 { background-position: -600px 0px; }
-.emoji-1F1E6-1F1E9 { background-position: -620px 0px; }
-.emoji-1F1E6-1F1EA { background-position: -640px 0px; }
-.emoji-1F1E6-1F1EB { background-position: -660px 0px; }
-.emoji-1F1E6-1F1EC { background-position: -680px 0px; }
-.emoji-1F1E6-1F1EE { background-position: -700px 0px; }
-.emoji-1F1E6-1F1F1 { background-position: -720px 0px; }
-.emoji-1F1E6-1F1F2 { background-position: -740px 0px; }
-.emoji-1F1E6-1F1F4 { background-position: -760px 0px; }
-.emoji-1F1E6-1F1F7 { background-position: -780px 0px; }
-.emoji-1F1E6-1F1F9 { background-position: -800px 0px; }
-.emoji-1F1E6-1F1FA { background-position: -820px 0px; }
-.emoji-1F1E6-1F1FC { background-position: -840px 0px; }
-.emoji-1F1E6-1F1FF { background-position: -860px 0px; }
-.emoji-1F1E7-1F1E6 { background-position: -880px 0px; }
-.emoji-1F1E7-1F1E7 { background-position: -900px 0px; }
-.emoji-1F1E7-1F1E9 { background-position: -920px 0px; }
-.emoji-1F1E7-1F1EA { background-position: -940px 0px; }
-.emoji-1F1E7-1F1EB { background-position: -960px 0px; }
-.emoji-1F1E7-1F1EC { background-position: -980px 0px; }
-.emoji-1F1E7-1F1ED { background-position: -1000px 0px; }
-.emoji-1F1E7-1F1EE { background-position: -1020px 0px; }
-.emoji-1F1E7-1F1EF { background-position: -1040px 0px; }
-.emoji-1F1E7-1F1F2 { background-position: -1060px 0px; }
-.emoji-1F1E7-1F1F3 { background-position: -1080px 0px; }
-.emoji-1F1E7-1F1F4 { background-position: -1100px 0px; }
-.emoji-1F1E7-1F1F7 { background-position: -1120px 0px; }
-.emoji-1F1E7-1F1F8 { background-position: -1140px 0px; }
-.emoji-1F1E7-1F1F9 { background-position: -1160px 0px; }
-.emoji-1F1E7-1F1FC { background-position: -1180px 0px; }
-.emoji-1F1E7-1F1FE { background-position: -1200px 0px; }
-.emoji-1F1E7-1F1FF { background-position: -1220px 0px; }
-.emoji-1F1E8-1F1E6 { background-position: -1240px 0px; }
-.emoji-1F1E8-1F1E9 { background-position: -1260px 0px; }
-.emoji-1F1E8-1F1EB { background-position: -1280px 0px; }
-.emoji-1F1E8-1F1EC { background-position: -1300px 0px; }
-.emoji-1F1E8-1F1ED { background-position: -1320px 0px; }
-.emoji-1F1E8-1F1EE { background-position: -1340px 0px; }
-.emoji-1F1E8-1F1F1 { background-position: -1360px 0px; }
-.emoji-1F1E8-1F1F2 { background-position: -1380px 0px; }
-.emoji-1F1E8-1F1F3 { background-position: -1400px 0px; }
-.emoji-1F1E8-1F1F4 { background-position: -1420px 0px; }
-.emoji-1F1E8-1F1F7 { background-position: -1440px 0px; }
-.emoji-1F1E8-1F1FA { background-position: -1460px 0px; }
-.emoji-1F1E8-1F1FB { background-position: -1480px 0px; }
-.emoji-1F1E8-1F1FE { background-position: -1500px 0px; }
-.emoji-1F1E8-1F1FF { background-position: -1520px 0px; }
-.emoji-1F1E9-1F1EA { background-position: -1540px 0px; }
-.emoji-1F1E9-1F1EF { background-position: -1560px 0px; }
-.emoji-1F1E9-1F1F0 { background-position: -1580px 0px; }
-.emoji-1F1E9-1F1F2 { background-position: -1600px 0px; }
-.emoji-1F1E9-1F1F4 { background-position: -1620px 0px; }
-.emoji-1F1E9-1F1FF { background-position: -1640px 0px; }
-.emoji-1F1EA-1F1E8 { background-position: -1660px 0px; }
-.emoji-1F1EA-1F1EA { background-position: -1680px 0px; }
-.emoji-1F1EA-1F1EC { background-position: -1700px 0px; }
-.emoji-1F1EA-1F1ED { background-position: -1720px 0px; }
-.emoji-1F1EA-1F1F7 { background-position: -1740px 0px; }
-.emoji-1F1EA-1F1F8 { background-position: -1760px 0px; }
-.emoji-1F1EA-1F1F9 { background-position: -1780px 0px; }
-.emoji-1F1EB-1F1EE { background-position: -1800px 0px; }
-.emoji-1F1EB-1F1EF { background-position: -1820px 0px; }
-.emoji-1F1EB-1F1F0 { background-position: -1840px 0px; }
-.emoji-1F1EB-1F1F2 { background-position: -1860px 0px; }
-.emoji-1F1EB-1F1F4 { background-position: -1880px 0px; }
-.emoji-1F1EB-1F1F7 { background-position: -1900px 0px; }
-.emoji-1F1EC-1F1E6 { background-position: -1920px 0px; }
-.emoji-1F1EC-1F1E7 { background-position: -1940px 0px; }
-.emoji-1F1EC-1F1E9 { background-position: -1960px 0px; }
-.emoji-1F1EC-1F1EA { background-position: -1980px 0px; }
-.emoji-1F1EC-1F1ED { background-position: -2000px 0px; }
-.emoji-1F1EC-1F1EE { background-position: -2020px 0px; }
-.emoji-1F1EC-1F1F1 { background-position: -2040px 0px; }
-.emoji-1F1EC-1F1F2 { background-position: -2060px 0px; }
-.emoji-1F1EC-1F1F3 { background-position: -2080px 0px; }
-.emoji-1F1EC-1F1F6 { background-position: -2100px 0px; }
-.emoji-1F1EC-1F1F7 { background-position: -2120px 0px; }
-.emoji-1F1EC-1F1F9 { background-position: -2140px 0px; }
-.emoji-1F1EC-1F1FA { background-position: -2160px 0px; }
-.emoji-1F1EC-1F1FC { background-position: -2180px 0px; }
-.emoji-1F1EC-1F1FE { background-position: -2200px 0px; }
-.emoji-1F1ED-1F1F0 { background-position: -2220px 0px; }
-.emoji-1F1ED-1F1F3 { background-position: -2240px 0px; }
-.emoji-1F1ED-1F1F7 { background-position: -2260px 0px; }
-.emoji-1F1ED-1F1F9 { background-position: -2280px 0px; }
-.emoji-1F1ED-1F1FA { background-position: -2300px 0px; }
-.emoji-1F1EE-1F1E9 { background-position: -2320px 0px; }
-.emoji-1F1EE-1F1EA { background-position: -2340px 0px; }
-.emoji-1F1EE-1F1F1 { background-position: -2360px 0px; }
-.emoji-1F1EE-1F1F3 { background-position: -2380px 0px; }
-.emoji-1F1EE-1F1F6 { background-position: -2400px 0px; }
-.emoji-1F1EE-1F1F7 { background-position: -2420px 0px; }
-.emoji-1F1EE-1F1F8 { background-position: -2440px 0px; }
-.emoji-1F1EE-1F1F9 { background-position: -2460px 0px; }
-.emoji-1F1EF-1F1EA { background-position: -2480px 0px; }
-.emoji-1F1EF-1F1F2 { background-position: -2500px 0px; }
-.emoji-1F1EF-1F1F4 { background-position: -2520px 0px; }
-.emoji-1F1EF-1F1F5 { background-position: -2540px 0px; }
-.emoji-1F1F0-1F1EA { background-position: -2560px 0px; }
-.emoji-1F1F0-1F1EC { background-position: -2580px 0px; }
-.emoji-1F1F0-1F1ED { background-position: -2600px 0px; }
-.emoji-1F1F0-1F1EE { background-position: -2620px 0px; }
-.emoji-1F1F0-1F1F2 { background-position: -2640px 0px; }
-.emoji-1F1F0-1F1F3 { background-position: -2660px 0px; }
-.emoji-1F1F0-1F1F5 { background-position: -2680px 0px; }
-.emoji-1F1F0-1F1F7 { background-position: -2700px 0px; }
-.emoji-1F1F0-1F1FC { background-position: -2720px 0px; }
-.emoji-1F1F0-1F1FE { background-position: -2740px 0px; }
-.emoji-1F1F0-1F1FF { background-position: -2760px 0px; }
-.emoji-1F1F1-1F1E6 { background-position: -2780px 0px; }
-.emoji-1F1F1-1F1E7 { background-position: -2800px 0px; }
-.emoji-1F1F1-1F1E8 { background-position: -2820px 0px; }
-.emoji-1F1F1-1F1EE { background-position: -2840px 0px; }
-.emoji-1F1F1-1F1F0 { background-position: -2860px 0px; }
-.emoji-1F1F1-1F1F7 { background-position: -2880px 0px; }
-.emoji-1F1F1-1F1F8 { background-position: -2900px 0px; }
-.emoji-1F1F1-1F1F9 { background-position: -2920px 0px; }
-.emoji-1F1F1-1F1FA { background-position: -2940px 0px; }
-.emoji-1F1F1-1F1FB { background-position: -2960px 0px; }
-.emoji-1F1F1-1F1FE { background-position: -2980px 0px; }
-.emoji-1F1F2-1F1E6 { background-position: -3000px 0px; }
-.emoji-1F1F2-1F1E8 { background-position: -3020px 0px; }
-.emoji-1F1F2-1F1E9 { background-position: -3040px 0px; }
-.emoji-1F1F2-1F1EA { background-position: -3060px 0px; }
-.emoji-1F1F2-1F1EC { background-position: -3080px 0px; }
-.emoji-1F1F2-1F1ED { background-position: -3100px 0px; }
-.emoji-1F1F2-1F1F0 { background-position: -3120px 0px; }
-.emoji-1F1F2-1F1F1 { background-position: -3140px 0px; }
-.emoji-1F1F2-1F1F2 { background-position: -3160px 0px; }
-.emoji-1F1F2-1F1F3 { background-position: -3180px 0px; }
-.emoji-1F1F2-1F1F4 { background-position: -3200px 0px; }
-.emoji-1F1F2-1F1F7 { background-position: -3220px 0px; }
-.emoji-1F1F2-1F1F8 { background-position: -3240px 0px; }
-.emoji-1F1F2-1F1F9 { background-position: -3260px 0px; }
-.emoji-1F1F2-1F1FA { background-position: -3280px 0px; }
-.emoji-1F1F2-1F1FB { background-position: -3300px 0px; }
-.emoji-1F1F2-1F1FC { background-position: -3320px 0px; }
-.emoji-1F1F2-1F1FD { background-position: -3340px 0px; }
-.emoji-1F1F2-1F1FE { background-position: -3360px 0px; }
-.emoji-1F1F2-1F1FF { background-position: -3380px 0px; }
-.emoji-1F1F3-1F1E6 { background-position: -3400px 0px; }
-.emoji-1F1F3-1F1E8 { background-position: -3420px 0px; }
-.emoji-1F1F3-1F1EA { background-position: -3440px 0px; }
-.emoji-1F1F3-1F1EC { background-position: -3460px 0px; }
-.emoji-1F1F3-1F1EE { background-position: -3480px 0px; }
-.emoji-1F1F3-1F1F1 { background-position: -3500px 0px; }
-.emoji-1F1F3-1F1F4 { background-position: -3520px 0px; }
-.emoji-1F1F3-1F1F5 { background-position: -3540px 0px; }
-.emoji-1F1F3-1F1F7 { background-position: -3560px 0px; }
-.emoji-1F1F3-1F1FA { background-position: -3580px 0px; }
-.emoji-1F1F3-1F1FF { background-position: -3600px 0px; }
-.emoji-1F1F4-1F1F2 { background-position: -3620px 0px; }
-.emoji-1F1F5-1F1E6 { background-position: -3640px 0px; }
-.emoji-1F1F5-1F1EA { background-position: -3660px 0px; }
-.emoji-1F1F5-1F1EB { background-position: -3680px 0px; }
-.emoji-1F1F5-1F1EC { background-position: -3700px 0px; }
-.emoji-1F1F5-1F1ED { background-position: -3720px 0px; }
-.emoji-1F1F5-1F1F0 { background-position: -3740px 0px; }
-.emoji-1F1F5-1F1F1 { background-position: -3760px 0px; }
-.emoji-1F1F5-1F1F7 { background-position: -3780px 0px; }
-.emoji-1F1F5-1F1F8 { background-position: -3800px 0px; }
-.emoji-1F1F5-1F1F9 { background-position: -3820px 0px; }
-.emoji-1F1F5-1F1FC { background-position: -3840px 0px; }
-.emoji-1F1F5-1F1FE { background-position: -3860px 0px; }
-.emoji-1F1F6-1F1E6 { background-position: -3880px 0px; }
-.emoji-1F1F7-1F1F4 { background-position: -3900px 0px; }
-.emoji-1F1F7-1F1F8 { background-position: -3920px 0px; }
-.emoji-1F1F7-1F1FA { background-position: -3940px 0px; }
-.emoji-1F1F7-1F1FC { background-position: -3960px 0px; }
-.emoji-1F1F8-1F1E6 { background-position: -3980px 0px; }
-.emoji-1F1F8-1F1E7 { background-position: -4000px 0px; }
-.emoji-1F1F8-1F1E8 { background-position: -4020px 0px; }
-.emoji-1F1F8-1F1E9 { background-position: -4040px 0px; }
-.emoji-1F1F8-1F1EA { background-position: -4060px 0px; }
-.emoji-1F1F8-1F1EC { background-position: -4080px 0px; }
-.emoji-1F1F8-1F1ED { background-position: -4100px 0px; }
-.emoji-1F1F8-1F1EE { background-position: -4120px 0px; }
-.emoji-1F1F8-1F1F0 { background-position: -4140px 0px; }
-.emoji-1F1F8-1F1F1 { background-position: -4160px 0px; }
-.emoji-1F1F8-1F1F2 { background-position: -4180px 0px; }
-.emoji-1F1F8-1F1F3 { background-position: -4200px 0px; }
-.emoji-1F1F8-1F1F4 { background-position: -4220px 0px; }
-.emoji-1F1F8-1F1F7 { background-position: -4240px 0px; }
-.emoji-1F1F8-1F1F9 { background-position: -4260px 0px; }
-.emoji-1F1F8-1F1FB { background-position: -4280px 0px; }
-.emoji-1F1F8-1F1FE { background-position: -4300px 0px; }
-.emoji-1F1F8-1F1FF { background-position: -4320px 0px; }
-.emoji-1F1F9-1F1E9 { background-position: -4340px 0px; }
-.emoji-1F1F9-1F1EC { background-position: -4360px 0px; }
-.emoji-1F1F9-1F1ED { background-position: -4380px 0px; }
-.emoji-1F1F9-1F1EF { background-position: -4400px 0px; }
-.emoji-1F1F9-1F1F1 { background-position: -4420px 0px; }
-.emoji-1F1F9-1F1F2 { background-position: -4440px 0px; }
-.emoji-1F1F9-1F1F3 { background-position: -4460px 0px; }
-.emoji-1F1F9-1F1F4 { background-position: -4480px 0px; }
-.emoji-1F1F9-1F1F7 { background-position: -4500px 0px; }
-.emoji-1F1F9-1F1F9 { background-position: -4520px 0px; }
-.emoji-1F1F9-1F1FB { background-position: -4540px 0px; }
-.emoji-1F1F9-1F1FC { background-position: -4560px 0px; }
-.emoji-1F1F9-1F1FF { background-position: -4580px 0px; }
-.emoji-1F1FA-1F1E6 { background-position: -4600px 0px; }
-.emoji-1F1FA-1F1EC { background-position: -4620px 0px; }
-.emoji-1F1FA-1F1F8 { background-position: -4640px 0px; }
-.emoji-1F1FA-1F1FE { background-position: -4660px 0px; }
-.emoji-1F1FA-1F1FF { background-position: -4680px 0px; }
-.emoji-1F1FB-1F1E6 { background-position: -4700px 0px; }
-.emoji-1F1FB-1F1E8 { background-position: -4720px 0px; }
-.emoji-1F1FB-1F1EA { background-position: -4740px 0px; }
-.emoji-1F1FB-1F1EE { background-position: -4760px 0px; }
-.emoji-1F1FB-1F1F3 { background-position: -4780px 0px; }
-.emoji-1F1FB-1F1FA { background-position: -4800px 0px; }
-.emoji-1F1FC-1F1EB { background-position: -4820px 0px; }
-.emoji-1F1FC-1F1F8 { background-position: -4840px 0px; }
-.emoji-1F1FD-1F1F0 { background-position: -4860px 0px; }
-.emoji-1F1FE-1F1EA { background-position: -4880px 0px; }
-.emoji-1F1FF-1F1E6 { background-position: -4900px 0px; }
-.emoji-1F1FF-1F1F2 { background-position: -4920px 0px; }
-.emoji-1F1FF-1F1FC { background-position: -4940px 0px; }
-.emoji-1F201 { background-position: -4960px 0px; }
-.emoji-1F202 { background-position: -4980px 0px; }
-.emoji-1F21A { background-position: -5000px 0px; }
-.emoji-1F22F { background-position: -5020px 0px; }
-.emoji-1F232 { background-position: -5040px 0px; }
-.emoji-1F233 { background-position: -5060px 0px; }
-.emoji-1F234 { background-position: -5080px 0px; }
-.emoji-1F235 { background-position: -5100px 0px; }
-.emoji-1F236 { background-position: -5120px 0px; }
-.emoji-1F237 { background-position: -5140px 0px; }
-.emoji-1F238 { background-position: -5160px 0px; }
-.emoji-1F239 { background-position: -5180px 0px; }
-.emoji-1F23A { background-position: -5200px 0px; }
-.emoji-1F250 { background-position: -5220px 0px; }
-.emoji-1F251 { background-position: -5240px 0px; }
-.emoji-1F300 { background-position: -5260px 0px; }
-.emoji-1F301 { background-position: -5280px 0px; }
-.emoji-1F302 { background-position: -5300px 0px; }
-.emoji-1F303 { background-position: -5320px 0px; }
-.emoji-1F304 { background-position: -5340px 0px; }
-.emoji-1F305 { background-position: -5360px 0px; }
-.emoji-1F306 { background-position: -5380px 0px; }
-.emoji-1F307 { background-position: -5400px 0px; }
-.emoji-1F308 { background-position: -5420px 0px; }
-.emoji-1F309 { background-position: -5440px 0px; }
-.emoji-1F30A { background-position: -5460px 0px; }
-.emoji-1F30B { background-position: -5480px 0px; }
-.emoji-1F30C { background-position: -5500px 0px; }
-.emoji-1F30D { background-position: -5520px 0px; }
-.emoji-1F30E { background-position: -5540px 0px; }
-.emoji-1F30F { background-position: -5560px 0px; }
-.emoji-1F310 { background-position: -5580px 0px; }
-.emoji-1F311 { background-position: -5600px 0px; }
-.emoji-1F312 { background-position: -5620px 0px; }
-.emoji-1F313 { background-position: -5640px 0px; }
-.emoji-1F314 { background-position: -5660px 0px; }
-.emoji-1F315 { background-position: -5680px 0px; }
-.emoji-1F316 { background-position: -5700px 0px; }
-.emoji-1F317 { background-position: -5720px 0px; }
-.emoji-1F318 { background-position: -5740px 0px; }
-.emoji-1F319 { background-position: -5760px 0px; }
-.emoji-1F31A { background-position: -5780px 0px; }
-.emoji-1F31B { background-position: -5800px 0px; }
-.emoji-1F31C { background-position: -5820px 0px; }
-.emoji-1F31D { background-position: -5840px 0px; }
-.emoji-1F31E { background-position: -5860px 0px; }
-.emoji-1F31F { background-position: -5880px 0px; }
-.emoji-1F320 { background-position: -5900px 0px; }
-.emoji-1F321 { background-position: -5920px 0px; }
-.emoji-1F327 { background-position: -5940px 0px; }
-.emoji-1F328 { background-position: -5960px 0px; }
-.emoji-1F329 { background-position: -5980px 0px; }
-.emoji-1F32A { background-position: -6000px 0px; }
-.emoji-1F32B { background-position: -6020px 0px; }
-.emoji-1F32C { background-position: -6040px 0px; }
-.emoji-1F330 { background-position: -6060px 0px; }
-.emoji-1F331 { background-position: -6080px 0px; }
-.emoji-1F332 { background-position: -6100px 0px; }
-.emoji-1F333 { background-position: -6120px 0px; }
-.emoji-1F334 { background-position: -6140px 0px; }
-.emoji-1F335 { background-position: -6160px 0px; }
-.emoji-1F336 { background-position: -6180px 0px; }
-.emoji-1F337 { background-position: -6200px 0px; }
-.emoji-1F338 { background-position: -6220px 0px; }
-.emoji-1F339 { background-position: -6240px 0px; }
-.emoji-1F33A { background-position: -6260px 0px; }
-.emoji-1F33B { background-position: -6280px 0px; }
-.emoji-1F33C { background-position: -6300px 0px; }
-.emoji-1F33D { background-position: -6320px 0px; }
-.emoji-1F33E { background-position: -6340px 0px; }
-.emoji-1F33F { background-position: -6360px 0px; }
-.emoji-1F340 { background-position: -6380px 0px; }
-.emoji-1F341 { background-position: -6400px 0px; }
-.emoji-1F342 { background-position: -6420px 0px; }
-.emoji-1F343 { background-position: -6440px 0px; }
-.emoji-1F344 { background-position: -6460px 0px; }
-.emoji-1F345 { background-position: -6480px 0px; }
-.emoji-1F346 { background-position: -6500px 0px; }
-.emoji-1F347 { background-position: -6520px 0px; }
-.emoji-1F348 { background-position: -6540px 0px; }
-.emoji-1F349 { background-position: -6560px 0px; }
-.emoji-1F34A { background-position: -6580px 0px; }
-.emoji-1F34B { background-position: -6600px 0px; }
-.emoji-1F34C { background-position: -6620px 0px; }
-.emoji-1F34D { background-position: -6640px 0px; }
-.emoji-1F34E { background-position: -6660px 0px; }
-.emoji-1F34F { background-position: -6680px 0px; }
-.emoji-1F350 { background-position: -6700px 0px; }
-.emoji-1F351 { background-position: -6720px 0px; }
-.emoji-1F352 { background-position: -6740px 0px; }
-.emoji-1F353 { background-position: -6760px 0px; }
-.emoji-1F354 { background-position: -6780px 0px; }
-.emoji-1F355 { background-position: -6800px 0px; }
-.emoji-1F356 { background-position: -6820px 0px; }
-.emoji-1F357 { background-position: -6840px 0px; }
-.emoji-1F358 { background-position: -6860px 0px; }
-.emoji-1F359 { background-position: -6880px 0px; }
-.emoji-1F35A { background-position: -6900px 0px; }
-.emoji-1F35B { background-position: -6920px 0px; }
-.emoji-1F35C { background-position: -6940px 0px; }
-.emoji-1F35D { background-position: -6960px 0px; }
-.emoji-1F35E { background-position: -6980px 0px; }
-.emoji-1F35F { background-position: -7000px 0px; }
-.emoji-1F360 { background-position: -7020px 0px; }
-.emoji-1F361 { background-position: -7040px 0px; }
-.emoji-1F362 { background-position: -7060px 0px; }
-.emoji-1F363 { background-position: -7080px 0px; }
-.emoji-1F364 { background-position: -7100px 0px; }
-.emoji-1F365 { background-position: -7120px 0px; }
-.emoji-1F366 { background-position: -7140px 0px; }
-.emoji-1F367 { background-position: -7160px 0px; }
-.emoji-1F368 { background-position: -7180px 0px; }
-.emoji-1F369 { background-position: -7200px 0px; }
-.emoji-1F36A { background-position: -7220px 0px; }
-.emoji-1F36B { background-position: -7240px 0px; }
-.emoji-1F36C { background-position: -7260px 0px; }
-.emoji-1F36D { background-position: -7280px 0px; }
-.emoji-1F36E { background-position: -7300px 0px; }
-.emoji-1F36F { background-position: -7320px 0px; }
-.emoji-1F370 { background-position: -7340px 0px; }
-.emoji-1F371 { background-position: -7360px 0px; }
-.emoji-1F372 { background-position: -7380px 0px; }
-.emoji-1F373 { background-position: -7400px 0px; }
-.emoji-1F374 { background-position: -7420px 0px; }
-.emoji-1F375 { background-position: -7440px 0px; }
-.emoji-1F376 { background-position: -7460px 0px; }
-.emoji-1F377 { background-position: -7480px 0px; }
-.emoji-1F378 { background-position: -7500px 0px; }
-.emoji-1F379 { background-position: -7520px 0px; }
-.emoji-1F37A { background-position: -7540px 0px; }
-.emoji-1F37B { background-position: -7560px 0px; }
-.emoji-1F37C { background-position: -7580px 0px; }
-.emoji-1F37D { background-position: -7600px 0px; }
-.emoji-1F380 { background-position: -7620px 0px; }
-.emoji-1F381 { background-position: -7640px 0px; }
-.emoji-1F382 { background-position: -7660px 0px; }
-.emoji-1F383 { background-position: -7680px 0px; }
-.emoji-1F384 { background-position: -7700px 0px; }
-.emoji-1F385 { background-position: -7720px 0px; }
-.emoji-1F386 { background-position: -7740px 0px; }
-.emoji-1F387 { background-position: -7760px 0px; }
-.emoji-1F388 { background-position: -7780px 0px; }
-.emoji-1F389 { background-position: -7800px 0px; }
-.emoji-1F38A { background-position: -7820px 0px; }
-.emoji-1F38B { background-position: -7840px 0px; }
-.emoji-1F38C { background-position: -7860px 0px; }
-.emoji-1F38D { background-position: -7880px 0px; }
-.emoji-1F38E { background-position: -7900px 0px; }
-.emoji-1F38F { background-position: -7920px 0px; }
-.emoji-1F390 { background-position: -7940px 0px; }
-.emoji-1F391 { background-position: -7960px 0px; }
-.emoji-1F392 { background-position: -7980px 0px; }
-.emoji-1F393 { background-position: -8000px 0px; }
-.emoji-1F394 { background-position: -8020px 0px; }
-.emoji-1F395 { background-position: -8040px 0px; }
-.emoji-1F396 { background-position: -8060px 0px; }
-.emoji-1F397 { background-position: -8080px 0px; }
-.emoji-1F398 { background-position: -8100px 0px; }
-.emoji-1F399 { background-position: -8120px 0px; }
-.emoji-1F39A { background-position: -8140px 0px; }
-.emoji-1F39B { background-position: -8160px 0px; }
-.emoji-1F39C { background-position: -8180px 0px; }
-.emoji-1F39D { background-position: -8200px 0px; }
-.emoji-1F39E { background-position: -8220px 0px; }
-.emoji-1F39F { background-position: -8240px 0px; }
-.emoji-1F3A0 { background-position: -8260px 0px; }
-.emoji-1F3A1 { background-position: -8280px 0px; }
-.emoji-1F3A2 { background-position: -8300px 0px; }
-.emoji-1F3A3 { background-position: -8320px 0px; }
-.emoji-1F3A4 { background-position: -8340px 0px; }
-.emoji-1F3A5 { background-position: -8360px 0px; }
-.emoji-1F3A6 { background-position: -8380px 0px; }
-.emoji-1F3A7 { background-position: -8400px 0px; }
-.emoji-1F3A8 { background-position: -8420px 0px; }
-.emoji-1F3A9 { background-position: -8440px 0px; }
-.emoji-1F3AA { background-position: -8460px 0px; }
-.emoji-1F3AB { background-position: -8480px 0px; }
-.emoji-1F3AC { background-position: -8500px 0px; }
-.emoji-1F3AD { background-position: -8520px 0px; }
-.emoji-1F3AE { background-position: -8540px 0px; }
-.emoji-1F3AF { background-position: -8560px 0px; }
-.emoji-1F3B0 { background-position: -8580px 0px; }
-.emoji-1F3B1 { background-position: -8600px 0px; }
-.emoji-1F3B2 { background-position: -8620px 0px; }
-.emoji-1F3B3 { background-position: -8640px 0px; }
-.emoji-1F3B4 { background-position: -8660px 0px; }
-.emoji-1F3B5 { background-position: -8680px 0px; }
-.emoji-1F3B6 { background-position: -8700px 0px; }
-.emoji-1F3B7 { background-position: -8720px 0px; }
-.emoji-1F3B8 { background-position: -8740px 0px; }
-.emoji-1F3B9 { background-position: -8760px 0px; }
-.emoji-1F3BA { background-position: -8780px 0px; }
-.emoji-1F3BB { background-position: -8800px 0px; }
-.emoji-1F3BC { background-position: -8820px 0px; }
-.emoji-1F3BD { background-position: -8840px 0px; }
-.emoji-1F3BE { background-position: -8860px 0px; }
-.emoji-1F3BF { background-position: -8880px 0px; }
-.emoji-1F3C0 { background-position: -8900px 0px; }
-.emoji-1F3C1 { background-position: -8920px 0px; }
-.emoji-1F3C2 { background-position: -8940px 0px; }
-.emoji-1F3C3 { background-position: -8960px 0px; }
-.emoji-1F3C4 { background-position: -8980px 0px; }
-.emoji-1F3C5 { background-position: -9000px 0px; }
-.emoji-1F3C6 { background-position: -9020px 0px; }
-.emoji-1F3C7 { background-position: -9040px 0px; }
-.emoji-1F3C8 { background-position: -9060px 0px; }
-.emoji-1F3C9 { background-position: -9080px 0px; }
-.emoji-1F3CA { background-position: -9100px 0px; }
-.emoji-1F3CB { background-position: -9120px 0px; }
-.emoji-1F3CC { background-position: -9140px 0px; }
-.emoji-1F3CD { background-position: -9160px 0px; }
-.emoji-1F3CE { background-position: -9180px 0px; }
-.emoji-1F3D4 { background-position: -9200px 0px; }
-.emoji-1F3D5 { background-position: -9220px 0px; }
-.emoji-1F3D6 { background-position: -9240px 0px; }
-.emoji-1F3D7 { background-position: -9260px 0px; }
-.emoji-1F3D8 { background-position: -9280px 0px; }
-.emoji-1F3D9 { background-position: -9300px 0px; }
-.emoji-1F3DA { background-position: -9320px 0px; }
-.emoji-1F3DB { background-position: -9340px 0px; }
-.emoji-1F3DC { background-position: -9360px 0px; }
-.emoji-1F3DD { background-position: -9380px 0px; }
-.emoji-1F3DE { background-position: -9400px 0px; }
-.emoji-1F3DF { background-position: -9420px 0px; }
-.emoji-1F3E0 { background-position: -9440px 0px; }
-.emoji-1F3E1 { background-position: -9460px 0px; }
-.emoji-1F3E2 { background-position: -9480px 0px; }
-.emoji-1F3E3 { background-position: -9500px 0px; }
-.emoji-1F3E4 { background-position: -9520px 0px; }
-.emoji-1F3E5 { background-position: -9540px 0px; }
-.emoji-1F3E6 { background-position: -9560px 0px; }
-.emoji-1F3E7 { background-position: -9580px 0px; }
-.emoji-1F3E8 { background-position: -9600px 0px; }
-.emoji-1F3E9 { background-position: -9620px 0px; }
-.emoji-1F3EA { background-position: -9640px 0px; }
-.emoji-1F3EB { background-position: -9660px 0px; }
-.emoji-1F3EC { background-position: -9680px 0px; }
-.emoji-1F3ED { background-position: -9700px 0px; }
-.emoji-1F3EE { background-position: -9720px 0px; }
-.emoji-1F3EF { background-position: -9740px 0px; }
-.emoji-1F3F0 { background-position: -9760px 0px; }
-.emoji-1F3F1 { background-position: -9780px 0px; }
-.emoji-1F3F2 { background-position: -9800px 0px; }
-.emoji-1F3F3 { background-position: -9820px 0px; }
-.emoji-1F3F4 { background-position: -9840px 0px; }
-.emoji-1F3F5 { background-position: -9860px 0px; }
-.emoji-1F3F6 { background-position: -9880px 0px; }
-.emoji-1F3F7 { background-position: -9900px 0px; }
-.emoji-1F400 { background-position: -9920px 0px; }
-.emoji-1F401 { background-position: -9940px 0px; }
-.emoji-1F402 { background-position: -9960px 0px; }
-.emoji-1F403 { background-position: -9980px 0px; }
-.emoji-1F404 { background-position: -10000px 0px; }
-.emoji-1F405 { background-position: -10020px 0px; }
-.emoji-1F406 { background-position: -10040px 0px; }
-.emoji-1F407 { background-position: -10060px 0px; }
-.emoji-1F408 { background-position: -10080px 0px; }
-.emoji-1F409 { background-position: -10100px 0px; }
-.emoji-1F40A { background-position: -10120px 0px; }
-.emoji-1F40B { background-position: -10140px 0px; }
-.emoji-1F40C { background-position: -10160px 0px; }
-.emoji-1F40D { background-position: -10180px 0px; }
-.emoji-1F40E { background-position: -10200px 0px; }
-.emoji-1F40F { background-position: -10220px 0px; }
-.emoji-1F410 { background-position: -10240px 0px; }
-.emoji-1F411 { background-position: -10260px 0px; }
-.emoji-1F412 { background-position: -10280px 0px; }
-.emoji-1F413 { background-position: -10300px 0px; }
-.emoji-1F414 { background-position: -10320px 0px; }
-.emoji-1F415 { background-position: -10340px 0px; }
-.emoji-1F416 { background-position: -10360px 0px; }
-.emoji-1F417 { background-position: -10380px 0px; }
-.emoji-1F418 { background-position: -10400px 0px; }
-.emoji-1F419 { background-position: -10420px 0px; }
-.emoji-1F41A { background-position: -10440px 0px; }
-.emoji-1F41B { background-position: -10460px 0px; }
-.emoji-1F41C { background-position: -10480px 0px; }
-.emoji-1F41D { background-position: -10500px 0px; }
-.emoji-1F41E { background-position: -10520px 0px; }
-.emoji-1F41F { background-position: -10540px 0px; }
-.emoji-1F420 { background-position: -10560px 0px; }
-.emoji-1F421 { background-position: -10580px 0px; }
-.emoji-1F422 { background-position: -10600px 0px; }
-.emoji-1F423 { background-position: -10620px 0px; }
-.emoji-1F424 { background-position: -10640px 0px; }
-.emoji-1F425 { background-position: -10660px 0px; }
-.emoji-1F426 { background-position: -10680px 0px; }
-.emoji-1F427 { background-position: -10700px 0px; }
-.emoji-1F428 { background-position: -10720px 0px; }
-.emoji-1F429 { background-position: -10740px 0px; }
-.emoji-1F42A { background-position: -10760px 0px; }
-.emoji-1F42B { background-position: -10780px 0px; }
-.emoji-1F42C { background-position: -10800px 0px; }
-.emoji-1F42D { background-position: -10820px 0px; }
-.emoji-1F42E { background-position: -10840px 0px; }
-.emoji-1F42F { background-position: -10860px 0px; }
-.emoji-1F430 { background-position: -10880px 0px; }
-.emoji-1F431 { background-position: -10900px 0px; }
-.emoji-1F432 { background-position: -10920px 0px; }
-.emoji-1F433 { background-position: -10940px 0px; }
-.emoji-1F434 { background-position: -10960px 0px; }
-.emoji-1F435 { background-position: -10980px 0px; }
-.emoji-1F436 { background-position: -11000px 0px; }
-.emoji-1F437 { background-position: -11020px 0px; }
-.emoji-1F438 { background-position: -11040px 0px; }
-.emoji-1F439 { background-position: -11060px 0px; }
-.emoji-1F43A { background-position: -11080px 0px; }
-.emoji-1F43B { background-position: -11100px 0px; }
-.emoji-1F43C { background-position: -11120px 0px; }
-.emoji-1F43D { background-position: -11140px 0px; }
-.emoji-1F43E { background-position: -11160px 0px; }
-.emoji-1F43F { background-position: -11180px 0px; }
-.emoji-1F440 { background-position: -11200px 0px; }
-.emoji-1F441 { background-position: -11220px 0px; }
-.emoji-1F442 { background-position: -11240px 0px; }
-.emoji-1F443 { background-position: -11260px 0px; }
-.emoji-1F444 { background-position: -11280px 0px; }
-.emoji-1F445 { background-position: -11300px 0px; }
-.emoji-1F446 { background-position: -11320px 0px; }
-.emoji-1F447 { background-position: -11340px 0px; }
-.emoji-1F448 { background-position: -11360px 0px; }
-.emoji-1F449 { background-position: -11380px 0px; }
-.emoji-1F44A { background-position: -11400px 0px; }
-.emoji-1F44B { background-position: -11420px 0px; }
-.emoji-1F44C { background-position: -11440px 0px; }
-.emoji-1F44D { background-position: -11460px 0px; }
-.emoji-1F44E { background-position: -11480px 0px; }
-.emoji-1F44F { background-position: -11500px 0px; }
-.emoji-1F450 { background-position: -11520px 0px; }
-.emoji-1F451 { background-position: -11540px 0px; }
-.emoji-1F452 { background-position: -11560px 0px; }
-.emoji-1F453 { background-position: -11580px 0px; }
-.emoji-1F454 { background-position: -11600px 0px; }
-.emoji-1F455 { background-position: -11620px 0px; }
-.emoji-1F456 { background-position: -11640px 0px; }
-.emoji-1F457 { background-position: -11660px 0px; }
-.emoji-1F458 { background-position: -11680px 0px; }
-.emoji-1F459 { background-position: -11700px 0px; }
-.emoji-1F45A { background-position: -11720px 0px; }
-.emoji-1F45B { background-position: -11740px 0px; }
-.emoji-1F45C { background-position: -11760px 0px; }
-.emoji-1F45D { background-position: -11780px 0px; }
-.emoji-1F45E { background-position: -11800px 0px; }
-.emoji-1F45F { background-position: -11820px 0px; }
-.emoji-1F460 { background-position: -11840px 0px; }
-.emoji-1F461 { background-position: -11860px 0px; }
-.emoji-1F462 { background-position: -11880px 0px; }
-.emoji-1F463 { background-position: -11900px 0px; }
-.emoji-1F464 { background-position: -11920px 0px; }
-.emoji-1F465 { background-position: -11940px 0px; }
-.emoji-1F466 { background-position: -11960px 0px; }
-.emoji-1F467 { background-position: -11980px 0px; }
-.emoji-1F468 { background-position: -12000px 0px; }
-.emoji-1F468-1F468-1F466 { background-position: -12020px 0px; }
-.emoji-1F468-1F468-1F466-1F466 { background-position: -12040px 0px; }
-.emoji-1F468-1F468-1F467 { background-position: -12060px 0px; }
-.emoji-1F468-1F468-1F467-1F466 { background-position: -12080px 0px; }
-.emoji-1F468-1F468-1F467-1F467 { background-position: -12100px 0px; }
-.emoji-1F468-1F469-1F466-1F466 { background-position: -12120px 0px; }
-.emoji-1F468-1F469-1F467 { background-position: -12140px 0px; }
-.emoji-1F468-1F469-1F467-1F466 { background-position: -12160px 0px; }
-.emoji-1F468-1F469-1F467-1F467 { background-position: -12180px 0px; }
-.emoji-1F468-2764-1F468 { background-position: -12200px 0px; }
-.emoji-1F468-2764-1F48B-1F468 { background-position: -12220px 0px; }
-.emoji-1F469 { background-position: -12240px 0px; }
-.emoji-1F469-1F469-1F466 { background-position: -12260px 0px; }
-.emoji-1F469-1F469-1F466-1F466 { background-position: -12280px 0px; }
-.emoji-1F469-1F469-1F467 { background-position: -12300px 0px; }
-.emoji-1F469-1F469-1F467-1F466 { background-position: -12320px 0px; }
-.emoji-1F469-1F469-1F467-1F467 { background-position: -12340px 0px; }
-.emoji-1F469-2764-1F469 { background-position: -12360px 0px; }
-.emoji-1F469-2764-1F48B-1F469 { background-position: -12380px 0px; }
-.emoji-1F46A { background-position: -12400px 0px; }
-.emoji-1F46B { background-position: -12420px 0px; }
-.emoji-1F46C { background-position: -12440px 0px; }
-.emoji-1F46D { background-position: -12460px 0px; }
-.emoji-1F46E { background-position: -12480px 0px; }
-.emoji-1F46F { background-position: -12500px 0px; }
-.emoji-1F470 { background-position: -12520px 0px; }
-.emoji-1F471 { background-position: -12540px 0px; }
-.emoji-1F472 { background-position: -12560px 0px; }
-.emoji-1F473 { background-position: -12580px 0px; }
-.emoji-1F474 { background-position: -12600px 0px; }
-.emoji-1F475 { background-position: -12620px 0px; }
-.emoji-1F476 { background-position: -12640px 0px; }
-.emoji-1F477 { background-position: -12660px 0px; }
-.emoji-1F478 { background-position: -12680px 0px; }
-.emoji-1F479 { background-position: -12700px 0px; }
-.emoji-1F47A { background-position: -12720px 0px; }
-.emoji-1F47B { background-position: -12740px 0px; }
-.emoji-1F47C { background-position: -12760px 0px; }
-.emoji-1F47D { background-position: -12780px 0px; }
-.emoji-1F47E { background-position: -12800px 0px; }
-.emoji-1F47F { background-position: -12820px 0px; }
-.emoji-1F480 { background-position: -12840px 0px; }
-.emoji-1F481 { background-position: -12860px 0px; }
-.emoji-1F482 { background-position: -12880px 0px; }
-.emoji-1F483 { background-position: -12900px 0px; }
-.emoji-1F484 { background-position: -12920px 0px; }
-.emoji-1F485 { background-position: -12940px 0px; }
-.emoji-1F486 { background-position: -12960px 0px; }
-.emoji-1F487 { background-position: -12980px 0px; }
-.emoji-1F488 { background-position: -13000px 0px; }
-.emoji-1F489 { background-position: -13020px 0px; }
-.emoji-1F48A { background-position: -13040px 0px; }
-.emoji-1F48B { background-position: -13060px 0px; }
-.emoji-1F48C { background-position: -13080px 0px; }
-.emoji-1F48D { background-position: -13100px 0px; }
-.emoji-1F48E { background-position: -13120px 0px; }
-.emoji-1F48F { background-position: -13140px 0px; }
-.emoji-1F490 { background-position: -13160px 0px; }
-.emoji-1F491 { background-position: -13180px 0px; }
-.emoji-1F492 { background-position: -13200px 0px; }
-.emoji-1F493 { background-position: -13220px 0px; }
-.emoji-1F494 { background-position: -13240px 0px; }
-.emoji-1F495 { background-position: -13260px 0px; }
-.emoji-1F496 { background-position: -13280px 0px; }
-.emoji-1F497 { background-position: -13300px 0px; }
-.emoji-1F498 { background-position: -13320px 0px; }
-.emoji-1F499 { background-position: -13340px 0px; }
-.emoji-1F49A { background-position: -13360px 0px; }
-.emoji-1F49B { background-position: -13380px 0px; }
-.emoji-1F49C { background-position: -13400px 0px; }
-.emoji-1F49D { background-position: -13420px 0px; }
-.emoji-1F49E { background-position: -13440px 0px; }
-.emoji-1F49F { background-position: -13460px 0px; }
-.emoji-1F4A0 { background-position: -13480px 0px; }
-.emoji-1F4A1 { background-position: -13500px 0px; }
-.emoji-1F4A2 { background-position: -13520px 0px; }
-.emoji-1F4A3 { background-position: -13540px 0px; }
-.emoji-1F4A4 { background-position: -13560px 0px; }
-.emoji-1F4A5 { background-position: -13580px 0px; }
-.emoji-1F4A6 { background-position: -13600px 0px; }
-.emoji-1F4A7 { background-position: -13620px 0px; }
-.emoji-1F4A8 { background-position: -13640px 0px; }
-.emoji-1F4A9 { background-position: -13660px 0px; }
-.emoji-1F4AA { background-position: -13680px 0px; }
-.emoji-1F4AB { background-position: -13700px 0px; }
-.emoji-1F4AC { background-position: -13720px 0px; }
-.emoji-1F4AD { background-position: -13740px 0px; }
-.emoji-1F4AE { background-position: -13760px 0px; }
-.emoji-1F4AF { background-position: -13780px 0px; }
-.emoji-1F4B0 { background-position: -13800px 0px; }
-.emoji-1F4B1 { background-position: -13820px 0px; }
-.emoji-1F4B2 { background-position: -13840px 0px; }
-.emoji-1F4B3 { background-position: -13860px 0px; }
-.emoji-1F4B4 { background-position: -13880px 0px; }
-.emoji-1F4B5 { background-position: -13900px 0px; }
-.emoji-1F4B6 { background-position: -13920px 0px; }
-.emoji-1F4B7 { background-position: -13940px 0px; }
-.emoji-1F4B8 { background-position: -13960px 0px; }
-.emoji-1F4B9 { background-position: -13980px 0px; }
-.emoji-1F4BA { background-position: -14000px 0px; }
-.emoji-1F4BB { background-position: -14020px 0px; }
-.emoji-1F4BC { background-position: -14040px 0px; }
-.emoji-1F4BD { background-position: -14060px 0px; }
-.emoji-1F4BE { background-position: -14080px 0px; }
-.emoji-1F4BF { background-position: -14100px 0px; }
-.emoji-1F4C0 { background-position: -14120px 0px; }
-.emoji-1F4C1 { background-position: -14140px 0px; }
-.emoji-1F4C2 { background-position: -14160px 0px; }
-.emoji-1F4C3 { background-position: -14180px 0px; }
-.emoji-1F4C4 { background-position: -14200px 0px; }
-.emoji-1F4C5 { background-position: -14220px 0px; }
-.emoji-1F4C6 { background-position: -14240px 0px; }
-.emoji-1F4C7 { background-position: -14260px 0px; }
-.emoji-1F4C8 { background-position: -14280px 0px; }
-.emoji-1F4C9 { background-position: -14300px 0px; }
-.emoji-1F4CA { background-position: -14320px 0px; }
-.emoji-1F4CB { background-position: -14340px 0px; }
-.emoji-1F4CC { background-position: -14360px 0px; }
-.emoji-1F4CD { background-position: -14380px 0px; }
-.emoji-1F4CE { background-position: -14400px 0px; }
-.emoji-1F4CF { background-position: -14420px 0px; }
-.emoji-1F4D0 { background-position: -14440px 0px; }
-.emoji-1F4D1 { background-position: -14460px 0px; }
-.emoji-1F4D2 { background-position: -14480px 0px; }
-.emoji-1F4D3 { background-position: -14500px 0px; }
-.emoji-1F4D4 { background-position: -14520px 0px; }
-.emoji-1F4D5 { background-position: -14540px 0px; }
-.emoji-1F4D6 { background-position: -14560px 0px; }
-.emoji-1F4D7 { background-position: -14580px 0px; }
-.emoji-1F4D8 { background-position: -14600px 0px; }
-.emoji-1F4D9 { background-position: -14620px 0px; }
-.emoji-1F4DA { background-position: -14640px 0px; }
-.emoji-1F4DB { background-position: -14660px 0px; }
-.emoji-1F4DC { background-position: -14680px 0px; }
-.emoji-1F4DD { background-position: -14700px 0px; }
-.emoji-1F4DE { background-position: -14720px 0px; }
-.emoji-1F4DF { background-position: -14740px 0px; }
-.emoji-1F4E0 { background-position: -14760px 0px; }
-.emoji-1F4E1 { background-position: -14780px 0px; }
-.emoji-1F4E2 { background-position: -14800px 0px; }
-.emoji-1F4E3 { background-position: -14820px 0px; }
-.emoji-1F4E4 { background-position: -14840px 0px; }
-.emoji-1F4E5 { background-position: -14860px 0px; }
-.emoji-1F4E6 { background-position: -14880px 0px; }
-.emoji-1F4E7 { background-position: -14900px 0px; }
-.emoji-1F4E8 { background-position: -14920px 0px; }
-.emoji-1F4E9 { background-position: -14940px 0px; }
-.emoji-1F4EA { background-position: -14960px 0px; }
-.emoji-1F4EB { background-position: -14980px 0px; }
-.emoji-1F4EC { background-position: -15000px 0px; }
-.emoji-1F4ED { background-position: -15020px 0px; }
-.emoji-1F4EE { background-position: -15040px 0px; }
-.emoji-1F4EF { background-position: -15060px 0px; }
-.emoji-1F4F0 { background-position: -15080px 0px; }
-.emoji-1F4F1 { background-position: -15100px 0px; }
-.emoji-1F4F2 { background-position: -15120px 0px; }
-.emoji-1F4F3 { background-position: -15140px 0px; }
-.emoji-1F4F4 { background-position: -15160px 0px; }
-.emoji-1F4F5 { background-position: -15180px 0px; }
-.emoji-1F4F6 { background-position: -15200px 0px; }
-.emoji-1F4F7 { background-position: -15220px 0px; }
-.emoji-1F4F8 { background-position: -15240px 0px; }
-.emoji-1F4F9 { background-position: -15260px 0px; }
-.emoji-1F4FA { background-position: -15280px 0px; }
-.emoji-1F4FB { background-position: -15300px 0px; }
-.emoji-1F4FC { background-position: -15320px 0px; }
-.emoji-1F4FD { background-position: -15340px 0px; }
-.emoji-1F4FE { background-position: -15360px 0px; }
-.emoji-1F500 { background-position: -15380px 0px; }
-.emoji-1F501 { background-position: -15400px 0px; }
-.emoji-1F502 { background-position: -15420px 0px; }
-.emoji-1F503 { background-position: -15440px 0px; }
-.emoji-1F504 { background-position: -15460px 0px; }
-.emoji-1F505 { background-position: -15480px 0px; }
-.emoji-1F506 { background-position: -15500px 0px; }
-.emoji-1F507 { background-position: -15520px 0px; }
-.emoji-1F508 { background-position: -15540px 0px; }
-.emoji-1F509 { background-position: -15560px 0px; }
-.emoji-1F50A { background-position: -15580px 0px; }
-.emoji-1F50B { background-position: -15600px 0px; }
-.emoji-1F50C { background-position: -15620px 0px; }
-.emoji-1F50D { background-position: -15640px 0px; }
-.emoji-1F50E { background-position: -15660px 0px; }
-.emoji-1F50F { background-position: -15680px 0px; }
-.emoji-1F510 { background-position: -15700px 0px; }
-.emoji-1F511 { background-position: -15720px 0px; }
-.emoji-1F512 { background-position: -15740px 0px; }
-.emoji-1F513 { background-position: -15760px 0px; }
-.emoji-1F514 { background-position: -15780px 0px; }
-.emoji-1F515 { background-position: -15800px 0px; }
-.emoji-1F516 { background-position: -15820px 0px; }
-.emoji-1F517 { background-position: -15840px 0px; }
-.emoji-1F518 { background-position: -15860px 0px; }
-.emoji-1F519 { background-position: -15880px 0px; }
-.emoji-1F51A { background-position: -15900px 0px; }
-.emoji-1F51B { background-position: -15920px 0px; }
-.emoji-1F51C { background-position: -15940px 0px; }
-.emoji-1F51D { background-position: -15960px 0px; }
-.emoji-1F51E { background-position: -15980px 0px; }
-.emoji-1F51F { background-position: -16000px 0px; }
-.emoji-1F520 { background-position: -16020px 0px; }
-.emoji-1F521 { background-position: -16040px 0px; }
-.emoji-1F522 { background-position: -16060px 0px; }
-.emoji-1F523 { background-position: -16080px 0px; }
-.emoji-1F524 { background-position: -16100px 0px; }
-.emoji-1F525 { background-position: -16120px 0px; }
-.emoji-1F526 { background-position: -16140px 0px; }
-.emoji-1F527 { background-position: -16160px 0px; }
-.emoji-1F528 { background-position: -16180px 0px; }
-.emoji-1F529 { background-position: -16200px 0px; }
-.emoji-1F52A { background-position: -16220px 0px; }
-.emoji-1F52B { background-position: -16240px 0px; }
-.emoji-1F52C { background-position: -16260px 0px; }
-.emoji-1F52D { background-position: -16280px 0px; }
-.emoji-1F52E { background-position: -16300px 0px; }
-.emoji-1F52F { background-position: -16320px 0px; }
-.emoji-1F530 { background-position: -16340px 0px; }
-.emoji-1F531 { background-position: -16360px 0px; }
-.emoji-1F532 { background-position: -16380px 0px; }
-.emoji-1F533 { background-position: -16400px 0px; }
-.emoji-1F534 { background-position: -16420px 0px; }
-.emoji-1F535 { background-position: -16440px 0px; }
-.emoji-1F536 { background-position: -16460px 0px; }
-.emoji-1F537 { background-position: -16480px 0px; }
-.emoji-1F538 { background-position: -16500px 0px; }
-.emoji-1F539 { background-position: -16520px 0px; }
-.emoji-1F53A { background-position: -16540px 0px; }
-.emoji-1F53B { background-position: -16560px 0px; }
-.emoji-1F53C { background-position: -16580px 0px; }
-.emoji-1F53D { background-position: -16600px 0px; }
-.emoji-1F546 { background-position: -16620px 0px; }
-.emoji-1F547 { background-position: -16640px 0px; }
-.emoji-1F548 { background-position: -16660px 0px; }
-.emoji-1F549 { background-position: -16680px 0px; }
-.emoji-1F54A { background-position: -16700px 0px; }
-.emoji-1F550 { background-position: -16720px 0px; }
-.emoji-1F551 { background-position: -16740px 0px; }
-.emoji-1F552 { background-position: -16760px 0px; }
-.emoji-1F553 { background-position: -16780px 0px; }
-.emoji-1F554 { background-position: -16800px 0px; }
-.emoji-1F555 { background-position: -16820px 0px; }
-.emoji-1F556 { background-position: -16840px 0px; }
-.emoji-1F557 { background-position: -16860px 0px; }
-.emoji-1F558 { background-position: -16880px 0px; }
-.emoji-1F559 { background-position: -16900px 0px; }
-.emoji-1F55A { background-position: -16920px 0px; }
-.emoji-1F55B { background-position: -16940px 0px; }
-.emoji-1F55C { background-position: -16960px 0px; }
-.emoji-1F55D { background-position: -16980px 0px; }
-.emoji-1F55E { background-position: -17000px 0px; }
-.emoji-1F55F { background-position: -17020px 0px; }
-.emoji-1F560 { background-position: -17040px 0px; }
-.emoji-1F561 { background-position: -17060px 0px; }
-.emoji-1F562 { background-position: -17080px 0px; }
-.emoji-1F563 { background-position: -17100px 0px; }
-.emoji-1F564 { background-position: -17120px 0px; }
-.emoji-1F565 { background-position: -17140px 0px; }
-.emoji-1F566 { background-position: -17160px 0px; }
-.emoji-1F567 { background-position: -17180px 0px; }
-.emoji-1F568 { background-position: -17200px 0px; }
-.emoji-1F569 { background-position: -17220px 0px; }
-.emoji-1F56A { background-position: -17240px 0px; }
-.emoji-1F56B { background-position: -17260px 0px; }
-.emoji-1F56C { background-position: -17280px 0px; }
-.emoji-1F56D { background-position: -17300px 0px; }
-.emoji-1F56E { background-position: -17320px 0px; }
-.emoji-1F56F { background-position: -17340px 0px; }
-.emoji-1F570 { background-position: -17360px 0px; }
-.emoji-1F571 { background-position: -17380px 0px; }
-.emoji-1F572 { background-position: -17400px 0px; }
-.emoji-1F573 { background-position: -17420px 0px; }
-.emoji-1F574 { background-position: -17440px 0px; }
-.emoji-1F575 { background-position: -17460px 0px; }
-.emoji-1F576 { background-position: -17480px 0px; }
-.emoji-1F577 { background-position: -17500px 0px; }
-.emoji-1F578 { background-position: -17520px 0px; }
-.emoji-1F579 { background-position: -17540px 0px; }
-.emoji-1F57B { background-position: -17560px 0px; }
-.emoji-1F57E { background-position: -17580px 0px; }
-.emoji-1F57F { background-position: -17600px 0px; }
-.emoji-1F581 { background-position: -17620px 0px; }
-.emoji-1F582 { background-position: -17640px 0px; }
-.emoji-1F583 { background-position: -17660px 0px; }
-.emoji-1F585 { background-position: -17680px 0px; }
-.emoji-1F586 { background-position: -17700px 0px; }
-.emoji-1F587 { background-position: -17720px 0px; }
-.emoji-1F588 { background-position: -17740px 0px; }
-.emoji-1F589 { background-position: -17760px 0px; }
-.emoji-1F58A { background-position: -17780px 0px; }
-.emoji-1F58B { background-position: -17800px 0px; }
-.emoji-1F58C { background-position: -17820px 0px; }
-.emoji-1F58D { background-position: -17840px 0px; }
-.emoji-1F58E { background-position: -17860px 0px; }
-.emoji-1F58F { background-position: -17880px 0px; }
-.emoji-1F590 { background-position: -17900px 0px; }
-.emoji-1F591 { background-position: -17920px 0px; }
-.emoji-1F592 { background-position: -17940px 0px; }
-.emoji-1F593 { background-position: -17960px 0px; }
-.emoji-1F594 { background-position: -17980px 0px; }
-.emoji-1F595 { background-position: -18000px 0px; }
-.emoji-1F596 { background-position: -18020px 0px; }
-.emoji-1F597 { background-position: -18040px 0px; }
-.emoji-1F598 { background-position: -18060px 0px; }
-.emoji-1F599 { background-position: -18080px 0px; }
-.emoji-1F59E { background-position: -18100px 0px; }
-.emoji-1F59F { background-position: -18120px 0px; }
-.emoji-1F5A5 { background-position: -18140px 0px; }
-.emoji-1F5A6 { background-position: -18160px 0px; }
-.emoji-1F5A7 { background-position: -18180px 0px; }
-.emoji-1F5A8 { background-position: -18200px 0px; }
-.emoji-1F5A9 { background-position: -18220px 0px; }
-.emoji-1F5AA { background-position: -18240px 0px; }
-.emoji-1F5AB { background-position: -18260px 0px; }
-.emoji-1F5AD { background-position: -18280px 0px; }
-.emoji-1F5AE { background-position: -18300px 0px; }
-.emoji-1F5AF { background-position: -18320px 0px; }
-.emoji-1F5B2 { background-position: -18340px 0px; }
-.emoji-1F5B3 { background-position: -18360px 0px; }
-.emoji-1F5B4 { background-position: -18380px 0px; }
-.emoji-1F5B8 { background-position: -18400px 0px; }
-.emoji-1F5B9 { background-position: -18420px 0px; }
-.emoji-1F5BC { background-position: -18440px 0px; }
-.emoji-1F5BD { background-position: -18460px 0px; }
-.emoji-1F5BE { background-position: -18480px 0px; }
-.emoji-1F5C0 { background-position: -18500px 0px; }
-.emoji-1F5C1 { background-position: -18520px 0px; }
-.emoji-1F5C2 { background-position: -18540px 0px; }
-.emoji-1F5C3 { background-position: -18560px 0px; }
-.emoji-1F5C4 { background-position: -18580px 0px; }
-.emoji-1F5C6 { background-position: -18600px 0px; }
-.emoji-1F5C7 { background-position: -18620px 0px; }
-.emoji-1F5C9 { background-position: -18640px 0px; }
-.emoji-1F5CA { background-position: -18660px 0px; }
-.emoji-1F5CE { background-position: -18680px 0px; }
-.emoji-1F5CF { background-position: -18700px 0px; }
-.emoji-1F5D0 { background-position: -18720px 0px; }
-.emoji-1F5D1 { background-position: -18740px 0px; }
-.emoji-1F5D2 { background-position: -18760px 0px; }
-.emoji-1F5D3 { background-position: -18780px 0px; }
-.emoji-1F5D4 { background-position: -18800px 0px; }
-.emoji-1F5D8 { background-position: -18820px 0px; }
-.emoji-1F5D9 { background-position: -18840px 0px; }
-.emoji-1F5DC { background-position: -18860px 0px; }
-.emoji-1F5DD { background-position: -18880px 0px; }
-.emoji-1F5DE { background-position: -18900px 0px; }
-.emoji-1F5E0 { background-position: -18920px 0px; }
-.emoji-1F5E1 { background-position: -18940px 0px; }
-.emoji-1F5E2 { background-position: -18960px 0px; }
-.emoji-1F5E3 { background-position: -18980px 0px; }
-.emoji-1F5E8 { background-position: -19000px 0px; }
-.emoji-1F5E9 { background-position: -19020px 0px; }
-.emoji-1F5EA { background-position: -19040px 0px; }
-.emoji-1F5EB { background-position: -19060px 0px; }
-.emoji-1F5EC { background-position: -19080px 0px; }
-.emoji-1F5ED { background-position: -19100px 0px; }
-.emoji-1F5EE { background-position: -19120px 0px; }
-.emoji-1F5EF { background-position: -19140px 0px; }
-.emoji-1F5F0 { background-position: -19160px 0px; }
-.emoji-1F5F1 { background-position: -19180px 0px; }
-.emoji-1F5F2 { background-position: -19200px 0px; }
-.emoji-1F5F3 { background-position: -19220px 0px; }
-.emoji-1F5F4 { background-position: -19240px 0px; }
-.emoji-1F5F5 { background-position: -19260px 0px; }
-.emoji-1F5F8 { background-position: -19280px 0px; }
-.emoji-1F5F9 { background-position: -19300px 0px; }
-.emoji-1F5FA { background-position: -19320px 0px; }
-.emoji-1F5FB { background-position: -19340px 0px; }
-.emoji-1F5FC { background-position: -19360px 0px; }
-.emoji-1F5FD { background-position: -19380px 0px; }
-.emoji-1F5FE { background-position: -19400px 0px; }
-.emoji-1F5FF { background-position: -19420px 0px; }
-.emoji-1F600 { background-position: -19440px 0px; }
-.emoji-1F601 { background-position: -19460px 0px; }
-.emoji-1F602 { background-position: -19480px 0px; }
-.emoji-1F603 { background-position: -19500px 0px; }
-.emoji-1F604 { background-position: -19520px 0px; }
-.emoji-1F605 { background-position: -19540px 0px; }
-.emoji-1F606 { background-position: -19560px 0px; }
-.emoji-1F607 { background-position: -19580px 0px; }
-.emoji-1F608 { background-position: -19600px 0px; }
-.emoji-1F609 { background-position: -19620px 0px; }
-.emoji-1F60A { background-position: -19640px 0px; }
-.emoji-1F60B { background-position: -19660px 0px; }
-.emoji-1F60C { background-position: -19680px 0px; }
-.emoji-1F60D { background-position: -19700px 0px; }
-.emoji-1F60E { background-position: -19720px 0px; }
-.emoji-1F60F { background-position: -19740px 0px; }
-.emoji-1F610 { background-position: -19760px 0px; }
-.emoji-1F611 { background-position: -19780px 0px; }
-.emoji-1F612 { background-position: -19800px 0px; }
-.emoji-1F613 { background-position: -19820px 0px; }
-.emoji-1F614 { background-position: -19840px 0px; }
-.emoji-1F615 { background-position: -19860px 0px; }
-.emoji-1F616 { background-position: -19880px 0px; }
-.emoji-1F617 { background-position: -19900px 0px; }
-.emoji-1F618 { background-position: -19920px 0px; }
-.emoji-1F619 { background-position: -19940px 0px; }
-.emoji-1F61A { background-position: -19960px 0px; }
-.emoji-1F61B { background-position: -19980px 0px; }
-.emoji-1F61C { background-position: -20000px 0px; }
-.emoji-1F61D { background-position: -20020px 0px; }
-.emoji-1F61E { background-position: -20040px 0px; }
-.emoji-1F61F { background-position: -20060px 0px; }
-.emoji-1F620 { background-position: -20080px 0px; }
-.emoji-1F621 { background-position: -20100px 0px; }
-.emoji-1F622 { background-position: -20120px 0px; }
-.emoji-1F623 { background-position: -20140px 0px; }
-.emoji-1F624 { background-position: -20160px 0px; }
-.emoji-1F625 { background-position: -20180px 0px; }
-.emoji-1F626 { background-position: -20200px 0px; }
-.emoji-1F627 { background-position: -20220px 0px; }
-.emoji-1F628 { background-position: -20240px 0px; }
-.emoji-1F629 { background-position: -20260px 0px; }
-.emoji-1F62A { background-position: -20280px 0px; }
-.emoji-1F62B { background-position: -20300px 0px; }
-.emoji-1F62C { background-position: -20320px 0px; }
-.emoji-1F62D { background-position: -20340px 0px; }
-.emoji-1F62E { background-position: -20360px 0px; }
-.emoji-1F62F { background-position: -20380px 0px; }
-.emoji-1F630 { background-position: -20400px 0px; }
-.emoji-1F631 { background-position: -20420px 0px; }
-.emoji-1F632 { background-position: -20440px 0px; }
-.emoji-1F633 { background-position: -20460px 0px; }
-.emoji-1F634 { background-position: -20480px 0px; }
-.emoji-1F635 { background-position: -20500px 0px; }
-.emoji-1F636 { background-position: -20520px 0px; }
-.emoji-1F637 { background-position: -20540px 0px; }
-.emoji-1F638 { background-position: -20560px 0px; }
-.emoji-1F639 { background-position: -20580px 0px; }
-.emoji-1F63A { background-position: -20600px 0px; }
-.emoji-1F63B { background-position: -20620px 0px; }
-.emoji-1F63C { background-position: -20640px 0px; }
-.emoji-1F63D { background-position: -20660px 0px; }
-.emoji-1F63E { background-position: -20680px 0px; }
-.emoji-1F63F { background-position: -20700px 0px; }
-.emoji-1F640 { background-position: -20720px 0px; }
-.emoji-1F641 { background-position: -20740px 0px; }
-.emoji-1F642 { background-position: -20760px 0px; }
-.emoji-1F645 { background-position: -20780px 0px; }
-.emoji-1F646 { background-position: -20800px 0px; }
-.emoji-1F647 { background-position: -20820px 0px; }
-.emoji-1F648 { background-position: -20840px 0px; }
-.emoji-1F649 { background-position: -20860px 0px; }
-.emoji-1F64A { background-position: -20880px 0px; }
-.emoji-1F64B { background-position: -20900px 0px; }
-.emoji-1F64C { background-position: -20920px 0px; }
-.emoji-1F64D { background-position: -20940px 0px; }
-.emoji-1F64E { background-position: -20960px 0px; }
-.emoji-1F64F { background-position: -20980px 0px; }
-.emoji-1F680 { background-position: -21000px 0px; }
-.emoji-1F681 { background-position: -21020px 0px; }
-.emoji-1F682 { background-position: -21040px 0px; }
-.emoji-1F683 { background-position: -21060px 0px; }
-.emoji-1F684 { background-position: -21080px 0px; }
-.emoji-1F685 { background-position: -21100px 0px; }
-.emoji-1F686 { background-position: -21120px 0px; }
-.emoji-1F687 { background-position: -21140px 0px; }
-.emoji-1F688 { background-position: -21160px 0px; }
-.emoji-1F689 { background-position: -21180px 0px; }
-.emoji-1F68A { background-position: -21200px 0px; }
-.emoji-1F68B { background-position: -21220px 0px; }
-.emoji-1F68C { background-position: -21240px 0px; }
-.emoji-1F68D { background-position: -21260px 0px; }
-.emoji-1F68E { background-position: -21280px 0px; }
-.emoji-1F68F { background-position: -21300px 0px; }
-.emoji-1F690 { background-position: -21320px 0px; }
-.emoji-1F691 { background-position: -21340px 0px; }
-.emoji-1F692 { background-position: -21360px 0px; }
-.emoji-1F693 { background-position: -21380px 0px; }
-.emoji-1F694 { background-position: -21400px 0px; }
-.emoji-1F695 { background-position: -21420px 0px; }
-.emoji-1F696 { background-position: -21440px 0px; }
-.emoji-1F697 { background-position: -21460px 0px; }
-.emoji-1F698 { background-position: -21480px 0px; }
-.emoji-1F699 { background-position: -21500px 0px; }
-.emoji-1F69A { background-position: -21520px 0px; }
-.emoji-1F69B { background-position: -21540px 0px; }
-.emoji-1F69C { background-position: -21560px 0px; }
-.emoji-1F69D { background-position: -21580px 0px; }
-.emoji-1F69E { background-position: -21600px 0px; }
-.emoji-1F69F { background-position: -21620px 0px; }
-.emoji-1F6A0 { background-position: -21640px 0px; }
-.emoji-1F6A1 { background-position: -21660px 0px; }
-.emoji-1F6A2 { background-position: -21680px 0px; }
-.emoji-1F6A3 { background-position: -21700px 0px; }
-.emoji-1F6A4 { background-position: -21720px 0px; }
-.emoji-1F6A5 { background-position: -21740px 0px; }
-.emoji-1F6A6 { background-position: -21760px 0px; }
-.emoji-1F6A7 { background-position: -21780px 0px; }
-.emoji-1F6A8 { background-position: -21800px 0px; }
-.emoji-1F6A9 { background-position: -21820px 0px; }
-.emoji-1F6AA { background-position: -21840px 0px; }
-.emoji-1F6AB { background-position: -21860px 0px; }
-.emoji-1F6AC { background-position: -21880px 0px; }
-.emoji-1F6AD { background-position: -21900px 0px; }
-.emoji-1F6AE { background-position: -21920px 0px; }
-.emoji-1F6AF { background-position: -21940px 0px; }
-.emoji-1F6B0 { background-position: -21960px 0px; }
-.emoji-1F6B1 { background-position: -21980px 0px; }
-.emoji-1F6B2 { background-position: -22000px 0px; }
-.emoji-1F6B3 { background-position: -22020px 0px; }
-.emoji-1F6B4 { background-position: -22040px 0px; }
-.emoji-1F6B5 { background-position: -22060px 0px; }
-.emoji-1F6B6 { background-position: -22080px 0px; }
-.emoji-1F6B7 { background-position: -22100px 0px; }
-.emoji-1F6B8 { background-position: -22120px 0px; }
-.emoji-1F6B9 { background-position: -22140px 0px; }
-.emoji-1F6BA { background-position: -22160px 0px; }
-.emoji-1F6BB { background-position: -22180px 0px; }
-.emoji-1F6BC { background-position: -22200px 0px; }
-.emoji-1F6BD { background-position: -22220px 0px; }
-.emoji-1F6BE { background-position: -22240px 0px; }
-.emoji-1F6BF { background-position: -22260px 0px; }
-.emoji-1F6C0 { background-position: -22280px 0px; }
-.emoji-1F6C1 { background-position: -22300px 0px; }
-.emoji-1F6C2 { background-position: -22320px 0px; }
-.emoji-1F6C3 { background-position: -22340px 0px; }
-.emoji-1F6C4 { background-position: -22360px 0px; }
-.emoji-1F6C5 { background-position: -22380px 0px; }
-.emoji-1F6C6 { background-position: -22400px 0px; }
-.emoji-1F6C7 { background-position: -22420px 0px; }
-.emoji-1F6C8 { background-position: -22440px 0px; }
-.emoji-1F6C9 { background-position: -22460px 0px; }
-.emoji-1F6CA { background-position: -22480px 0px; }
-.emoji-1F6CB { background-position: -22500px 0px; }
-.emoji-1F6CC { background-position: -22520px 0px; }
-.emoji-1F6CD { background-position: -22540px 0px; }
-.emoji-1F6CE { background-position: -22560px 0px; }
-.emoji-1F6CF { background-position: -22580px 0px; }
-.emoji-1F6E0 { background-position: -22600px 0px; }
-.emoji-1F6E1 { background-position: -22620px 0px; }
-.emoji-1F6E2 { background-position: -22640px 0px; }
-.emoji-1F6E3 { background-position: -22660px 0px; }
-.emoji-1F6E4 { background-position: -22680px 0px; }
-.emoji-1F6E5 { background-position: -22700px 0px; }
-.emoji-1F6E6 { background-position: -22720px 0px; }
-.emoji-1F6E7 { background-position: -22740px 0px; }
-.emoji-1F6E8 { background-position: -22760px 0px; }
-.emoji-1F6E9 { background-position: -22780px 0px; }
-.emoji-1F6EA { background-position: -22800px 0px; }
-.emoji-1F6EB { background-position: -22820px 0px; }
-.emoji-1F6EC { background-position: -22840px 0px; }
-.emoji-1F6F0 { background-position: -22860px 0px; }
-.emoji-1F6F1 { background-position: -22880px 0px; }
-.emoji-1F6F2 { background-position: -22900px 0px; }
-.emoji-1F6F3 { background-position: -22920px 0px; }
-.emoji-203C { background-position: -22940px 0px; }
-.emoji-2049 { background-position: -22960px 0px; }
-.emoji-2122 { background-position: -22980px 0px; }
-.emoji-2139 { background-position: -23000px 0px; }
-.emoji-2194 { background-position: -23020px 0px; }
-.emoji-2195 { background-position: -23040px 0px; }
-.emoji-2196 { background-position: -23060px 0px; }
-.emoji-2197 { background-position: -23080px 0px; }
-.emoji-2198 { background-position: -23100px 0px; }
-.emoji-2199 { background-position: -23120px 0px; }
-.emoji-21A9 { background-position: -23140px 0px; }
-.emoji-21AA { background-position: -23160px 0px; }
-.emoji-231A { background-position: -23180px 0px; }
-.emoji-231B { background-position: -23200px 0px; }
-.emoji-23E9 { background-position: -23220px 0px; }
-.emoji-23EA { background-position: -23240px 0px; }
-.emoji-23EB { background-position: -23260px 0px; }
-.emoji-23EC { background-position: -23280px 0px; }
-.emoji-23F0 { background-position: -23300px 0px; }
-.emoji-23F3 { background-position: -23320px 0px; }
-.emoji-24C2 { background-position: -23340px 0px; }
-.emoji-25AA { background-position: -23360px 0px; }
-.emoji-25AB { background-position: -23380px 0px; }
-.emoji-25B6 { background-position: -23400px 0px; }
-.emoji-25C0 { background-position: -23420px 0px; }
-.emoji-25FB { background-position: -23440px 0px; }
-.emoji-25FC { background-position: -23460px 0px; }
-.emoji-25FD { background-position: -23480px 0px; }
-.emoji-25FE { background-position: -23500px 0px; }
-.emoji-2600 { background-position: -23520px 0px; }
-.emoji-2601 { background-position: -23540px 0px; }
-.emoji-260E { background-position: -23560px 0px; }
-.emoji-2611 { background-position: -23580px 0px; }
-.emoji-2614 { background-position: -23600px 0px; }
-.emoji-2615 { background-position: -23620px 0px; }
-.emoji-261D { background-position: -23640px 0px; }
-.emoji-263A { background-position: -23660px 0px; }
-.emoji-2648 { background-position: -23680px 0px; }
-.emoji-2649 { background-position: -23700px 0px; }
-.emoji-264A { background-position: -23720px 0px; }
-.emoji-264B { background-position: -23740px 0px; }
-.emoji-264C { background-position: -23760px 0px; }
-.emoji-264D { background-position: -23780px 0px; }
-.emoji-264E { background-position: -23800px 0px; }
-.emoji-264F { background-position: -23820px 0px; }
-.emoji-2650 { background-position: -23840px 0px; }
-.emoji-2651 { background-position: -23860px 0px; }
-.emoji-2652 { background-position: -23880px 0px; }
-.emoji-2653 { background-position: -23900px 0px; }
-.emoji-2660 { background-position: -23920px 0px; }
-.emoji-2663 { background-position: -23940px 0px; }
-.emoji-2665 { background-position: -23960px 0px; }
-.emoji-2666 { background-position: -23980px 0px; }
-.emoji-2668 { background-position: -24000px 0px; }
-.emoji-267B { background-position: -24020px 0px; }
-.emoji-267F { background-position: -24040px 0px; }
-.emoji-2693 { background-position: -24060px 0px; }
-.emoji-26A0 { background-position: -24080px 0px; }
-.emoji-26A1 { background-position: -24100px 0px; }
-.emoji-26AA { background-position: -24120px 0px; }
-.emoji-26AB { background-position: -24140px 0px; }
-.emoji-26BD { background-position: -24160px 0px; }
-.emoji-26BE { background-position: -24180px 0px; }
-.emoji-26C4 { background-position: -24200px 0px; }
-.emoji-26C5 { background-position: -24220px 0px; }
-.emoji-26CE { background-position: -24240px 0px; }
-.emoji-26D4 { background-position: -24260px 0px; }
-.emoji-26EA { background-position: -24280px 0px; }
-.emoji-26F2 { background-position: -24300px 0px; }
-.emoji-26F3 { background-position: -24320px 0px; }
-.emoji-26F5 { background-position: -24340px 0px; }
-.emoji-26FA { background-position: -24360px 0px; }
-.emoji-26FD { background-position: -24380px 0px; }
-.emoji-2702 { background-position: -24400px 0px; }
-.emoji-2705 { background-position: -24420px 0px; }
-.emoji-2708 { background-position: -24440px 0px; }
-.emoji-2709 { background-position: -24460px 0px; }
-.emoji-270A { background-position: -24480px 0px; }
-.emoji-270B { background-position: -24500px 0px; }
-.emoji-270C { background-position: -24520px 0px; }
-.emoji-270F { background-position: -24540px 0px; }
-.emoji-2712 { background-position: -24560px 0px; }
-.emoji-2714 { background-position: -24580px 0px; }
-.emoji-2716 { background-position: -24600px 0px; }
-.emoji-2728 { background-position: -24620px 0px; }
-.emoji-2733 { background-position: -24640px 0px; }
-.emoji-2734 { background-position: -24660px 0px; }
-.emoji-2744 { background-position: -24680px 0px; }
-.emoji-2747 { background-position: -24700px 0px; }
-.emoji-274C { background-position: -24720px 0px; }
-.emoji-274E { background-position: -24740px 0px; }
-.emoji-2753 { background-position: -24760px 0px; }
-.emoji-2754 { background-position: -24780px 0px; }
-.emoji-2755 { background-position: -24800px 0px; }
-.emoji-2757 { background-position: -24820px 0px; }
-.emoji-2764 { background-position: -24840px 0px; }
-.emoji-2795 { background-position: -24860px 0px; }
-.emoji-2796 { background-position: -24880px 0px; }
-.emoji-2797 { background-position: -24900px 0px; }
-.emoji-27A1 { background-position: -24920px 0px; }
-.emoji-27B0 { background-position: -24940px 0px; }
-.emoji-27BF { background-position: -24960px 0px; }
-.emoji-2934 { background-position: -24980px 0px; }
-.emoji-2935 { background-position: -25000px 0px; }
-.emoji-2B05 { background-position: -25020px 0px; }
-.emoji-2B06 { background-position: -25040px 0px; }
-.emoji-2B07 { background-position: -25060px 0px; }
-.emoji-2B1B { background-position: -25080px 0px; }
-.emoji-2B1C { background-position: -25100px 0px; }
-.emoji-2B50 { background-position: -25120px 0px; }
-.emoji-2B55 { background-position: -25140px 0px; }
-.emoji-3030 { background-position: -25160px 0px; }
-.emoji-303D { background-position: -25180px 0px; }
-.emoji-3297 { background-position: -25200px 0px; }
-.emoji-3299 { background-position: -25220px 0px; } \ No newline at end of file
+ @media only screen and (-webkit-min-device-pixel-ratio: 2),
+ only screen and (min--moz-device-pixel-ratio: 2),
+ only screen and (-o-min-device-pixel-ratio: 2/1),
+ only screen and (min-device-pixel-ratio: 2),
+ only screen and (min-resolution: 192dpi),
+ only screen and (min-resolution: 2dppx) {
+ background-image: image-url('emoji@2x.png');
+ background-size: 840px 820px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 282aaf2219b..b39a9abf40f 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -4,9 +4,7 @@
*/
.event-item {
font-size: $gl-font-size;
- padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px);
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
+ padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
border-bottom: 1px solid $table-border-color;
color: #7f8fa4;
@@ -18,7 +16,7 @@
.event-title,
.event-item-timestamp {
- line-height: 44px;
+ line-height: 40px;
}
}
@@ -27,7 +25,7 @@
}
.avatar {
- margin-left: -($gl-avatar-size + 15px);
+ margin-left: -($gl-avatar-size + $gl-padding-top);
}
.event-title {
@@ -43,7 +41,6 @@
margin-right: 174px;
.event-note {
- margin-top: 5px;
word-wrap: break-word;
.md {
@@ -66,7 +63,7 @@
.note-image-attach {
margin-top: 4px;
- margin-left: 0px;
+ margin-left: 0;
max-width: 200px;
float: none;
}
@@ -86,10 +83,10 @@
.event_icon {
position: relative;
float: right;
- border: 1px solid #EEE;
+ border: 1px solid #eee;
padding: 5px;
@include border-radius(5px);
- background: #F9F9F9;
+ background: #f9f9f9;
margin-left: 10px;
top: -6px;
img {
@@ -97,11 +94,9 @@
}
}
- &:last-child { border:none }
+ &:last-child { border: none }
.event_commits {
- margin-top: 9px;
-
li {
&.commit {
background: transparent;
@@ -138,11 +133,12 @@
*/
.event-last-push {
overflow: auto;
+ width: 100%;
.event-last-push-text {
@include str-truncated(100%);
padding: 5px 0;
font-size: 13px;
- float:left;
+ float: left;
margin-right: -150px;
padding-right: 150px;
line-height: 20px;
@@ -164,7 +160,7 @@
.event-body {
margin: 0;
- border-left: 2px solid #DDD;
+ border-left: 2px solid #ddd;
padding-left: 10px;
}
diff --git a/app/assets/stylesheets/pages/explore.scss b/app/assets/stylesheets/pages/explore.scss
index da06fe9954e..9b92128624c 100644
--- a/app/assets/stylesheets/pages/explore.scss
+++ b/app/assets/stylesheets/pages/explore.scss
@@ -6,11 +6,3 @@
font-size: 30px;
}
}
-
-.explore-trending-block {
- .lead {
- line-height: 32px;
- font-size: 18px;
- margin-top: 10px;
- }
-}
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index c3b10d144e1..4e5c4ed84b6 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -6,11 +6,11 @@
font-size: 14px;
padding: 5px;
border-bottom: 1px solid $border-color;
- background: #EEE;
+ background: #eee;
}
.network-graph {
- background: #FFF;
+ background: #fff;
height: 500px;
overflow-y: scroll;
overflow-x: hidden;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 263993f59a5..ec6c099df5b 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -1,5 +1,15 @@
.member-search-form {
float: left;
+
+ input[type='search'] {
+ width: 225px;
+ vertical-align: bottom;
+
+ @media (max-width: $screen-xs-max) {
+ width: 100px;
+ vertical-align: bottom;
+ }
+ }
}
.milestone-row {
@@ -11,3 +21,21 @@
height: 42px;
}
}
+
+.group-row {
+ &.no-description {
+ .group-name {
+ line-height: 44px;
+ }
+ }
+
+ .stats {
+ float: right;
+ line-height: 44px;
+ color: $gl-gray;
+
+ span {
+ margin-right: 15px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss
index 3df4bb84bd2..6a99cd9cb94 100644
--- a/app/assets/stylesheets/pages/import.scss
+++ b/app/assets/stylesheets/pages/import.scss
@@ -1,6 +1,6 @@
i.icon-gitorious {
display: inline-block;
- background-position: 0px 0px;
+ background-position: 0 0;
background-size: contain;
background-repeat: no-repeat;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 9da273a0b6b..6f93299404c 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -20,25 +20,17 @@
position: fixed;
top: 70px;
margin-right: 35px;
- }
- }
-}
-
-.project-issuable-filter {
- .controls {
- float: right;
- margin-top: 7px;
- }
- .center-top-menu {
- text-align: left;
+ &.no-affix {
+ position: relative;
+ top: 0;
+ }
+ }
}
}
.issuable-details {
section {
- border-right: 1px solid $border-white-light;
-
.issuable-discussion {
margin-right: 1px;
}
@@ -67,12 +59,42 @@
.issuable-sidebar {
.block {
@include clearfix;
- padding: $gl-padding 0;
- border-bottom: 1px solid #F0F0F0;
+ padding: $gl-padding 0;
+ border-bottom: 1px solid $border-gray-light;
+ // This prevents the mess when resizing the sidebar
+ // of elements repositioning themselves..
+ width: $gutter_inner_width;
+ // --
+
+ &:first-child {
+ padding-top: 5px;
+ }
&:last-child {
border: none;
}
+
+ span {
+ margin-top: 7px;
+ display: inline-block;
+ }
+
+ .select2-container span {
+ margin-top: 0;
+ }
+
+ .issuable-count {
+
+ }
+
+ .gutter-toggle {
+ margin-left: 20px;
+ padding-left: 10px;
+
+ &:hover {
+ color: $gray-darkest;
+ }
+ }
}
.title {
@@ -94,11 +116,24 @@
}
.cross-project-reference {
- font-weight: bold;
color: $gl-link-color;
+ span {
+ white-space: nowrap;
+ width: 85%;
+ overflow: hidden;
+ position: relative;
+ display: inline-block;
+ text-overflow: ellipsis;
+ }
+
+ cite {
+ font-style: normal;
+ }
+
button {
float: right;
+ padding: 3px 5px;
}
}
@@ -115,3 +150,123 @@
margin-right: 2px;
}
}
+
+.right-sidebar {
+ position: fixed;
+ top: 58px;
+ bottom: 0;
+ right: 0;
+ transition: width .3s;
+ background: $gray-light;
+ padding: 10px 20px;
+
+ &.right-sidebar-expanded {
+ width: $gutter_width;
+
+ hr {
+ display: none;
+ }
+
+ .sidebar-collapsed-icon {
+ display: none;
+ }
+
+ .gutter-toggle {
+ border-left: 1px solid $border-gray-light;
+ }
+ }
+
+ .subscribe-button {
+ span {
+ margin-top: 0;
+ }
+ }
+
+ &.right-sidebar-collapsed {
+ /* Extra small devices (phones, less than 768px) */
+ display: none;
+ /* Small devices (tablets, 768px and up) */
+ @media (min-width: $screen-sm-min) {
+ display: block
+ }
+
+ width: $sidebar_collapsed_width;
+ padding-top: 0;
+
+ hr {
+ margin: 0;
+ color: $gray-normal;
+ border-color: $gray-normal;
+ width: 62px;
+ margin-left: -20px
+ }
+
+ .block {
+ width: $sidebar_collapsed_width - 1px;
+ margin-left: -19px;
+ padding: 15px 0 0 0;
+ border-bottom: none;
+ overflow: hidden;
+ }
+
+ .hide-collapsed {
+ display: none;
+ }
+
+ .gutter-toggle {
+ margin-left: -36px;
+ }
+
+ .sidebar-collapsed-icon {
+ display: block;
+ width: 100%;
+ text-align: center;
+ padding-bottom: 10px;
+ color: #999;
+
+ span {
+ display: block;
+ margin-top: 0;
+ }
+
+ .btn-clipboard {
+ border: none;
+
+ &:hover {
+ background: transparent;
+ }
+
+ i {
+ color: #999;
+ }
+ }
+ }
+ }
+
+ .btn {
+ background: $gray-normal;
+ border: 1px solid $border-gray-normal;
+ &:hover {
+ background: $gray-dark;
+ border: 1px solid $border-gray-dark;
+ }
+ }
+}
+
+.btn-default.gutter-toggle {
+ margin-top: 4px;
+}
+
+.detail-page-description {
+ small {
+ color: $gray-darkest;
+ }
+}
+
+.edited-text {
+ color: $gray-darkest;
+
+ .author_link {
+ color: $gray-darkest;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index a02a3a72e79..7ac4bc468d6 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -4,13 +4,7 @@
position: relative;
.issue-title {
- margin-bottom: 5px;
- font-size: $list-font-size;
- font-weight: bold;
- }
-
- .issue-info {
- color: $gl-gray;
+ margin-bottom: 2px;
}
.issue-check {
@@ -49,18 +43,13 @@
.issue-search-form {
margin: 0;
height: 24px;
-
- .issue_search {
- border: 1px solid #DDD !important;
- background-color: #f4f4f4;
- }
}
form.edit-issue {
margin: 0;
}
-.merge-requests-title {
+.merge-requests-title, .related-branches-title {
font-size: 16px;
font-weight: 600;
}
@@ -70,10 +59,6 @@ form.edit-issue {
width: 3em;
}
-.merge-request-info {
- padding-left: 5px;
-}
-
.merge-request-status {
color: $gl-gray;
font-size: 15px;
@@ -83,18 +68,18 @@ form.edit-issue {
.merge-request,
.issue {
&.today {
- background: #EFE;
- border-color: #CEC;
+ background: #efe;
+ border-color: #cec;
}
&.closed {
- background: #F9F9F9;
- border-color: #E5E5E5;
+ background: #f9f9f9;
+ border-color: #e5e5e5;
}
&.merged {
- background: #F9F9F9;
- border-color: #E5E5E5;
+ background: #f9f9f9;
+ border-color: #e5e5e5;
}
}
@@ -114,18 +99,17 @@ form.edit-issue {
.btn {
width: 100%;
- margin-top: -1px;
&:first-child:not(:last-child) {
- border-radius: 4px 4px 0 0;
+
}
&:not(:first-child):not(:last-child) {
- border-radius: 0;
+ margin-top: 10px;
}
&:last-child:not(:first-child) {
- border-radius: 0 0 4px 4px;
+ margin-top: 10px;
}
}
}
@@ -144,3 +128,16 @@ form.edit-issue {
.issue-form .select2-container {
width: 250px !important;
}
+
+.issue-closed-by-widget {
+ color: $secondary-text;
+ margin-left: 52px;
+}
+
+.editor-details {
+ display: block;
+
+ @media (min-width: $screen-sm-min) {
+ display: inline-block;
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index d1590e42fcb..61ee34b695e 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -7,9 +7,31 @@
display: inline-block;
margin-right: 10px;
}
+
+ &.suggest-colors-dropdown {
+ margin-bottom: 5px;
+
+ a {
+ @include border-radius(0);
+ width: 36.7px;
+ margin-right: 0;
+ margin-bottom: -5px;
+ }
+ }
+}
+
+.dropdown-label-color-preview {
+ display: none;
+ margin-top: 5px;
+ width: 100%;
+ height: 25px;
+
+ &.is-active {
+ display: block;
+ }
}
-.manage-labels-list {
+.label-row {
.label {
padding: 9px;
font-size: 14px;
@@ -19,3 +41,7 @@
.color-label {
padding: 3px 4px;
}
+
+.label-subscription {
+ display: inline-block;
+}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index f9c6f1b39f9..bc41f7d306f 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -8,6 +8,10 @@
max-width: none;
}
+ .flash-container {
+ margin-bottom: $gl-padding;
+ }
+
.brand-holder {
font-size: 18px;
line-height: 1.5;
@@ -24,7 +28,7 @@
img {
max-width: 100%;
- margin-bottom: 30px;
+ margin-bottom: 30px;
}
a {
@@ -35,7 +39,7 @@
.login-box{
background: #fafafa;
border-radius: 10px;
- box-shadow: 0 0px 2px #CCC;
+ box-shadow: 0 0 2px #ccc;
padding: 15px;
.login-heading h3 {
@@ -70,7 +74,7 @@
&.top {
@include border-radius(5px 5px 0 0);
- margin-bottom: 0px;
+ margin-bottom: 0;
}
&.bottom {
@@ -81,12 +85,12 @@
&.middle {
border-top: 0;
- margin-bottom:0px;
+ margin-bottom: 0;
@include border-radius(0);
}
&:active, &:focus {
- background-color: #FFF;
+ background-color: #fff;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 82effde0bf3..cee5c47cfb2 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,9 +3,9 @@
*
*/
.mr-state-widget {
- background: #F7F8FA;
+ background: $background-color;
color: $gl-gray;
- border: 1px solid #dce0e6;
+ border: 1px solid $border-color;
@include border-radius(2px);
form {
@@ -113,7 +113,7 @@
}
.mr-widget-footer {
- border-top: 1px solid #EEE;
+ border-top: 1px solid #eee;
}
.ci-coverage {
@@ -148,15 +148,8 @@
position: relative;
.merge-request-title {
- margin-bottom: 5px;
- font-size: $list-font-size;
- font-weight: bold;
+ margin-bottom: 2px;
}
-
- .merge-request-info {
- color: $gl-gray;
- }
-
}
.merge-request-labels {
@@ -201,3 +194,39 @@
.mr-source-target {
line-height: 31px;
}
+
+.disabled-comment-area {
+ padding: 16px 0;
+
+ .disabled-profile {
+ width: 40px;
+ height: 40px;
+ background: $border-gray-dark;
+ border-radius: 20px;
+ display: inline-block;
+ margin-right: 10px;
+ }
+
+ .disabled-comment {
+ background: $gray-light;
+ display: inline-block;
+ vertical-align: top;
+ height: 200px;
+ border-radius: 4px;
+ border: 1px solid $border-gray-normal;
+ padding-top: 90px;
+ text-align: center;
+ right: 20px;
+ position: absolute;
+ left: 70px;
+ margin-bottom: 20px;
+
+ span {
+ color: #b2b2b2;
+
+ a {
+ color: $md-link-color;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index e80dc9e84a1..d0e72a4422c 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -11,3 +11,56 @@ li.milestone {
height: 6px;
}
}
+
+.milestone-content {
+ .issues-count {
+ margin-right: 17px;
+ float: right;
+ width: 105px;
+ }
+
+ .issuable-row {
+ .color-label {
+ border-radius: 2px;
+ padding: 3px !important;
+ margin-right: 7px;
+ }
+
+ // Issue title
+ span a {
+ color: rgba(0,0,0,0.64);
+ }
+ }
+}
+
+.milestone-summary {
+ margin-bottom: 25px;
+
+ .milestone-stat {
+ margin-right: 10px;
+ }
+
+ .remaining-days {
+ color: $orange-light;
+ }
+}
+
+.issues-sortable-list, .merge_requests-sortable-list {
+ .issuable-detail {
+ display: block;
+ margin-top: 7px;
+
+ .issuable-number {
+ color: rgba(0,0,0,0.44);
+ margin-right: 5px;
+ }
+ .avatar {
+ float: none;
+ }
+ }
+}
+
+.milestone-detail {
+ border-bottom: 1px solid $border-color;
+ padding: 20px 0;
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index d86259f93fb..61783ec46aa 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -10,18 +10,6 @@
margin: 10px $gl-padding;
}
.diff-file .diff-content {
- tr.line_holder:hover {
- &> td.line_content {
- background: $hover !important;
- border-color: darken($hover, 10%) !important;
- }
- &> td.new_line,
- &> td.old_line {
- background: darken($hover, 4%) !important;
- border-color: darken($hover, 10%) !important;
- }
- }
-
tr.line_holder:hover > td .line_note_link {
opacity: 1.0;
filter: alpha(opacity=100);
@@ -159,6 +147,7 @@
.edit_note {
.markdown-area {
min-height: 140px;
+ max-height: 500px;
}
.note-form-actions {
background: transparent;
@@ -167,7 +156,7 @@
.comment-hints {
color: #999;
- background: #FFF;
+ background: #fff;
padding: 7px;
margin-top: -7px;
border: 1px solid $border-color;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 72b0ed29a69..d408853cc80 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -3,22 +3,34 @@
*/
@-webkit-keyframes targe3-note {
- from { background:#fffff0; }
- 50% { background:#ffffd3; }
- to { background:#fffff0; }
+ from { background: #fffff0; }
+ 50% { background: #ffffd3; }
+ to { background: #fffff0; }
}
ul.notes {
display: block;
list-style: none;
- margin: 0px;
- padding: 0px;
+ margin: 0;
+ padding: 0;
+
+ .timeline-icon {
+ float: left;
+ }
+
+ .timeline-content {
+ margin-left: 55px;
+ }
+
+ .note_created_ago, .note-updated-at {
+ white-space: nowrap;
+ }
.system-note {
font-size: 14px;
padding-top: 10px;
padding-bottom: 10px;
- background: #FDFDFD;
+ background: #fdfdfd;
.timeline-icon {
.avatar {
@@ -81,12 +93,12 @@ ul.notes {
.discussion {
overflow: hidden;
display: block;
- position:relative;
+ position: relative;
}
.note {
display: block;
- position:relative;
+ position: relative;
.note-body {
overflow: auto;
@@ -96,6 +108,13 @@ ul.notes {
word-wrap: break-word;
@include md-typography;
+ // On diffs code should wrap nicely and not overflow
+ pre {
+ code {
+ white-space: pre-wrap;
+ }
+ }
+
// Reset ul style types since we're nested inside a ul already
& > ul {
list-style-type: disc;
@@ -117,7 +136,7 @@ ul.notes {
hr {
// Darken 'whitesmoke' a bit to make it more visible in note bodies
- border-color: darken(#F5F5F5, 8%);
+ border-color: darken(#f5f5f5, 8%);
margin: 10px 0;
}
}
@@ -151,9 +170,11 @@ ul.notes {
border-left: none;
&.notes_line {
+ vertical-align: middle;
text-align: center;
padding: 10px 0;
- background: #FFF;
+ background: #fff;
+ color: $text-color;
}
&.notes_line2 {
text-align: center;
@@ -218,7 +239,7 @@ ul.notes {
.add-diff-note {
margin-top: -4px;
@include border-radius(40px);
- background: #FFF;
+ background: #fff;
padding: 4px;
font-size: 16px;
color: $gl-link-color;
@@ -235,18 +256,15 @@ ul.notes {
&:hover {
background: $gl-info;
- color: #FFF;
+ color: #fff;
@include show-add-diff-note;
}
}
// "show" the icon also if we just hover somewhere over the line
&:hover > td {
- background: $hover !important;
-
.add-diff-note {
@include show-add-diff-note;
}
}
}
-
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index cc273f55222..94fbbef3c77 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -1,16 +1,18 @@
-.global-notifications-form .level-title {
- font-size: 15px;
- color: #333;
- font-weight: bold;
+.notification-list-item {
+ line-height: 34px;
}
-.notification-icon-holder {
- width: 20px;
- float: left;
+.notification {
+ position: relative;
+ top: 1px;
+
+ > .fa {
+ font-size: 18px;
+ }
}
.ns-part {
- color: $gl-primary;
+ color: $gl-text-green;
}
.ns-watch {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 95fc26a608a..260179074cf 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -1,16 +1,27 @@
-.account-page {
- fieldset {
- margin-bottom: 15px;
- padding-bottom: 15px;
- }
-}
-
.profile-avatar-form-option {
hr {
margin: 10px 0;
}
}
+.avatar-image {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ margin-bottom: 0;
+ }
+}
+
+.avatar-file-name {
+ position: relative;
+ top: 2px;
+ display: inline-block;
+}
+
+.account-btn-link,
+.profile-settings-sidebar a {
+ color: $md-link-color;
+}
+
.oauth-buttons {
.btn-group {
margin-right: 10px;
@@ -19,7 +30,7 @@
.btn {
line-height: 40px;
height: 42px;
- padding: 0px 12px;
+ padding: 0 12px;
img {
width: 32px;
@@ -42,6 +53,18 @@
}
}
+.account-well {
+ padding: 10px 10px;
+ background-color: $help-well-bg;
+ border: 1px solid $help-well-border;
+ border-radius: $border-radius-base;
+
+ ul {
+ padding-left: 20px;
+ margin-bottom: 0;
+ }
+}
+
.calendar-hint {
margin-top: -12px;
float: right;
@@ -51,9 +74,17 @@
.profile-link-holder {
display: inline;
+ a {
+ color: $blue-dark;
+ text-decoration: none;
+ }
+}
+
+// Middle dot divider between each element in a list of items.
+.middle-dot-divider {
&:after {
- content: "\00B7";
- padding: 0px 6px;
+ content: "\00B7"; // Middle Dot
+ padding: 0 6px;
font-weight: bold;
}
@@ -63,9 +94,106 @@
padding: 0;
}
}
+}
+
+.profile-user-bio {
+ // Limits the width of the user bio for readability.
+ max-width: 750px;
+ margin: auto;
+}
+
+.user-avatar-button {
+ .file-name {
+ display: inline-block;
+ padding-left: 10px;
+ }
+}
+
+.key-list-item {
+ .key-list-item-info {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ }
+ }
+
+ .description {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+}
+
+.key-icon {
+ color: $ssh-key-icon-color;
+ font-size: $ssh-key-icon-size;
+ line-height: 42px;
+}
+
+.key-created-at {
+ line-height: 42px;
+}
+.profile-settings-content {
a {
- color: $blue-dark;
- text-decoration: none;
+ color: $md-link-color;
+ }
+}
+
+.change-username-title {
+ color: $gl-warning;
+}
+
+.remove-account-title {
+ color: $gl-danger;
+}
+
+.provider-btn-group {
+ display: inline-block;
+ margin-right: 10px;
+ border: 1px solid $provider-btn-group-border;
+ border-radius: 3px;
+
+ &:last-child {
+ margin-right: 0;
+ }
+}
+
+.provider-btn-image {
+ display: inline-block;
+ padding: 5px 10px;
+ border-right: 1px solid $provider-btn-group-border;
+
+ > img {
+ width: 20px;
+ }
+}
+
+.provider-btn {
+ display: inline-block;
+ padding: 5px 10px;
+ margin-left: -3px;
+ line-height: 22px;
+ background-color: $gray-light;
+
+ &.not-active {
+ color: $provider-btn-not-active-color;
+ }
+}
+
+.profile-settings-message {
+ line-height: 32px;
+ color: $warning-message-color;
+ background-color: $warning-message-bg;
+ border: 1px solid $warning-message-border;
+ border-radius: $border-radius-base;
+}
+
+.oauth-applications {
+ form {
+ display: inline-block;
+ }
+
+ .last-heading {
+ width: 105px;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index cff3edb7ed2..82c5069638d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -26,6 +26,23 @@
}
.project-home-panel {
+ padding-bottom: 40px;
+ border-bottom: 1px solid $border-color;
+
+ .cover-controls {
+ .project-settings-dropdown {
+ margin-left: 10px;
+ display: inline-block;
+
+ .dropdown-menu {
+ left: auto;
+ width: auto;
+ right: 0px;
+ max-width: 240px;
+ }
+ }
+ }
+
.project-identicon-holder {
margin-bottom: 16px;
@@ -39,11 +56,9 @@
}
}
- .project-home-dropdown {
- margin: 13px 0px 0;
- }
-
.notifications-btn {
+ margin-top: -28px;
+
.fa-bell {
margin-right: 6px;
}
@@ -62,53 +77,44 @@
font-weight: normal;
}
+ .visibility-icon {
+ display: inline-block;
+ margin-left: 5px;
+ font-size: 18px;
+ color: $gray;
+ }
+
p {
padding: 0 $gl-padding;
color: #5c5d5e;
}
}
- .git-clone-holder {
- max-width: 498px;
-
- .form-control {
- background: #FFF;
- font-size: 14px;
- height: 42px;
- margin-left: -1px;
- }
- }
-
- .visibility-level-label {
- @extend .btn;
- @extend .btn-gray;
-
- color: $gray;
- cursor: default;
-
- i {
- color: inherit;
- }
- }
-
- .git-clone-holder {
- display: inline-table;
- position: relative;
- }
-
.project-repo-buttons {
- margin-top: 12px;
- margin-bottom: 0px;
+ margin-top: 20px;
+ margin-bottom: 0;
.count-buttons {
display: block;
- margin-bottom: 12px;
+ margin-bottom: 20px;
+ }
+
+ .clone-row {
+ .split-repo-buttons,
+ .project-clone-holder {
+ display: inline-block;
+ }
+
+ .split-repo-buttons {
+ margin: 0 12px;
+ }
}
.btn {
@include btn-gray;
text-transform: none;
}
+
.count-with-arrow {
display: inline-block;
position: relative;
@@ -141,7 +147,7 @@
left: 1px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
- border-right-color: #FFF;
+ border-right-color: #fff;
}
}
.count {
@@ -153,20 +159,20 @@
border-style: solid;
font-size: 13px;
font-weight: 600;
- line-height: 20px;
- padding: 11px 16px;
+ line-height: 13px;
+ padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px;
- padding: 10px;
+ padding: 10px 14px;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
background-image: none;
white-space: nowrap;
- margin: 0 11px 0px 4px;
+ margin: 0 11px 0 4px;
&:hover {
- background: #FFF;
+ background: #fff;
}
}
}
@@ -182,141 +188,6 @@
}
}
-.git-clone-holder {
- .project-home-dropdown + & {
- margin-right: 45px;
- }
-
- .clone-options {
- display: table-cell;
- a.btn {
- width: 100%;
- }
- }
-
- .form-control {
- cursor: auto;
- @extend .monospace;
- background: #FAFAFA;
- width: 101%;
- }
-
- .input-group-addon {
- background: #f7f8fa;
-
- &.git-protocols {
- padding: 0;
- border: none;
-
- .input-group-btn:last-child > .btn {
- @include border-radius-right(0);
-
- border-left: 1px solid #c6cacf;
- margin-left: -2px !important;
- }
- }
- }
-}
-
-.projects-search-form {
-
- .input-group .form-control {
- height: 42px;
- }
-}
-
-.input-group-btn {
- .btn {
- @include btn-gray;
- @include btn-middle;
-
- &:hover {
- outline: none;
- }
-
- &:focus {
- outline: none;
- }
-
- &:active {
- outline: none;
- }
-
- &.btn-clipboard {
- padding-left: 15px;
- padding-right: 15px;
- }
- }
-
- .active {
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
-
- border: 1px solid #c6cacf !important;
- background-color: #e4e7ed !important;
- }
-
- .btn-green {
- @include btn-green
- }
-
-}
-
-.split-repo-buttons {
- display: inline-table;
- margin: 0 12px 0 12px;
-
- .btn{
- @include btn-gray;
- @include btn-default;
- }
-
- .dropdown-toggle {
- margin: -5px;
- }
-}
-
-#notification-form {
- margin-left: 5px;
-}
-
-.dropdown-new {
- margin-left: -5px;
-}
-
-.open > .dropdown-new.btn {
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
-
- border: 1px solid #c6cacf !important;
- background-color: #e4e7ed !important;
- text-transform: uppercase;
- color: #313236 !important;
- font-size: 13px;
- font-weight: 600;
-}
-
-.dropdown-menu {
- @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
- @include border-radius (0px);
-
- border: none;
- padding: 16px 0;
- font-size: 14px;
- font-weight: 100;
-
- li a {
- color: #5f697a;
- line-height: 30px;
-
- &:hover {
- background-color: #3084bb !important;
- }
- }
-
- i {
- margin-right: 8px;
- }
-}
-
.project-visibility-level-holder {
.radio {
margin-bottom: 10px;
@@ -345,30 +216,8 @@
color: #555;
}
-ul.nav.nav-projects-tabs {
- @extend .nav-tabs;
-
- padding-left: 8px;
-
- li {
- a {
- padding: 6px 25px;
- margin-top: 2px;
- border-color: #DDD;
- background-color: #EEE;
- text-shadow: 0 1px 1px white;
- color: #555;
- }
- &.active {
- a {
- font-weight: bold;
- }
- }
- }
-}
-
.project_member_row form {
- margin: 0px;
+ margin: 0;
}
.transfer-project .select2-container {
@@ -393,9 +242,9 @@ ul.nav.nav-projects-tabs {
.breadcrumb.repo-breadcrumb {
padding: 0;
- line-height: 42px;
background: transparent;
border: none;
+ line-height: 42px;
margin: 0;
> li + li:before {
@@ -404,36 +253,8 @@ ul.nav.nav-projects-tabs {
}
}
-.top-area {
- border-bottom: 1px solid #EEE;
- margin: 0 -16px;
- padding: 0 $gl-padding;
-
- ul.left-top-menu {
- display: inline-block;
- width: 50%;
- margin-bottom: 0px;
- border-bottom: none;
- }
-
- .projects-search-form {
- width: 50%;
- display: inline-block;
- float: right;
- padding-top: 7px;
- text-align: right;
-
- .btn-green {
- margin-top: -2px;
- margin-left: 10px;
- }
- }
-
- @media (max-width: $screen-xs-max) {
- .projects-search-form {
- padding-top: 15px;
- }
- }
+.last-push-widget {
+ margin-top: -1px;
}
.fork-namespaces {
@@ -471,12 +292,12 @@ table.table.protected-branches-list tr.no-border {
padding-top: 10px;
padding-bottom: 4px;
- ul.nav-pills {
- display:inline-block;
+ ul.nav {
+ display: inline-block;
}
- .nav-pills li {
- display:inline;
+ .nav li {
+ display: inline;
}
.nav > li > a {
@@ -489,11 +310,11 @@ table.table.protected-branches-list tr.no-border {
}
li {
- display:inline;
+ display: inline;
}
a {
- float:left;
+ float: left;
font-size: 17px;
}
@@ -511,22 +332,6 @@ pre.light-well {
border-color: #f1f1f1;
}
-.projects-search-form {
- margin: -$gl-padding;
- padding: $gl-padding;
- margin-bottom: 0px;
-
- input {
- display: inline-block;
- width: calc(100% - 151px);
- }
-
- .btn {
- display: inline-block;
- width: 135px;
- }
-}
-
.git-empty {
margin: 0 7px 0 7px;
@@ -562,41 +367,27 @@ pre.light-well {
@include basic-list;
.project-row {
- padding: $gl-padding;
border-color: $table-border-color;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
&.no-description {
.project {
- line-height: 44px;
+ line-height: 40px;
}
}
.project-full-name {
@include str-truncated;
- font-weight: 600;
- color: #4c4e54;
}
- .project-controls {
- float: right;
- color: $gl-gray;
- line-height: 45px;
- color: #7f8fa4;
+ .controls {
+ line-height: 40px;
a:hover {
text-decoration: none;
}
- }
-
- .project-description {
- color: #7f8fa4;
- p {
- @include str-truncated;
- margin-bottom: 0;
- color: #7f8fa4;
+ > span {
+ margin-left: 10px;
}
}
}
@@ -619,8 +410,6 @@ pre.light-well {
}
.project-last-commit {
- margin: 0 7px;
-
.ci-status {
margin-right: 16px;
}
@@ -650,9 +439,7 @@ pre.light-well {
}
.project-show-readme .readme-holder {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- padding: ($gl-padding + 7px);
+ padding: $gl-padding 0;
border-top: 0;
.edit-project-readme {
@@ -660,3 +447,48 @@ pre.light-well {
position: relative;
}
}
+
+.git-clone-holder {
+ width: 498px;
+
+ .btn-clipboard {
+ border: 1px solid $border-color;
+ padding: 6px $gl-padding;
+ }
+
+ .project-home-dropdown + & {
+ margin-right: 45px;
+ }
+
+ .clone-options {
+ display: table-cell;
+ a.btn {
+ width: 100%;
+ }
+ }
+
+ .form-control {
+ @extend .monospace;
+ background: #fff;
+ font-size: 14px;
+ margin-left: -1px;
+ cursor: auto;
+ width: 101%;
+ }
+}
+
+.cannot-be-merged,
+.cannot-be-merged:hover {
+ color: #e62958;
+ margin-top: 2px;
+}
+
+.private-forks-notice .private-fork-icon {
+ i:nth-child(1) {
+ color: #2aa056;
+ }
+
+ i:nth-child(2) {
+ color: #fff;
+ }
+}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index a9111a7388f..eec22c5dc96 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -1,7 +1,7 @@
.runner-state {
padding: 6px 12px;
margin-right: 10px;
- color: #FFF;
+ color: #fff;
&.runner-state-shared {
background: #32b186;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 3aaa96da609..b6e45024644 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -1,8 +1,12 @@
.search-results {
.search-result-row {
- border-bottom: 1px solid #DDD;
- padding-bottom: 15px;
- margin-bottom: 15px;
+ border-bottom: 1px solid $border-color;
+ padding-bottom: $gl-padding;
+ margin-bottom: $gl-padding;
+
+ &:last-child {
+ border-bottom: none;
+ }
}
}
@@ -12,7 +16,7 @@
margin-bottom: 20px;
input {
- border-color: #BBB;
+ border-color: #bbb;
font-weight: bold;
}
}
diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss
index 92d84d9640f..bed6470dbd3 100644
--- a/app/assets/stylesheets/pages/sherlock.scss
+++ b/app/assets/stylesheets/pages/sherlock.scss
@@ -13,13 +13,13 @@ table .sherlock-code {
}
.sherlock-line-samples-table {
- margin-bottom: 0px !important;
+ margin-bottom: 0 !important;
thead tr th,
tbody tr td {
font-size: 13px !important;
text-align: right;
- padding: 0px 10px !important;
+ padding: 0 10px !important;
}
}
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 1430d01859d..639d639d5b0 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -2,30 +2,6 @@
padding: 2px;
}
-
-.snippet-row {
- .snippet-title {
- font-size: 15px;
- font-weight: bold;
- line-height: 20px;
- margin-bottom: 2px;
-
- .monospace {
- font-weight: normal;
- }
- }
-
- .snippet-info {
- color: #888;
- font-size: 13px;
- line-height: 24px;
-
- a {
- color: #888;
- }
- }
-}
-
.snippet-holder {
margin-bottom: -$gl-padding;
@@ -50,5 +26,13 @@
margin-right: 10px;
font-size: $gl-font-size;
border: 1px solid;
- line-height: 40px;
+ line-height: 32px;
+}
+
+.markdown-snippet-copy {
+ position: fixed;
+ top: -10px;
+ left: -10px;
+ max-height: 0;
+ max-width: 0;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 4b6ef035673..6f777d11641 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,7 +1,7 @@
.ci-status {
padding: 2px 7px;
margin-right: 5px;
- border: 1px solid #EEE;
+ border: 1px solid #eee;
white-space: nowrap;
@include border-radius(4px);
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
new file mode 100644
index 00000000000..27970eba159
--- /dev/null
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -0,0 +1,96 @@
+/**
+ * Dashboard Todos
+ *
+ */
+
+.navbar-nav {
+ li {
+ .badge.todos-pending-count {
+ background-color: #7f8fa4;
+ margin-top: -5px;
+ font-weight: normal;
+ }
+ }
+}
+
+.todo-item {
+ font-size: $gl-font-size;
+ padding-left: $gl-avatar-size + $gl-padding-top;
+ color: $secondary-text;
+
+ a {
+ color: #4c4e54;
+ }
+
+ .avatar {
+ margin-left: -($gl-avatar-size + $gl-padding-top);
+ }
+
+ .todo-title {
+ @include str-truncated(calc(100% - 174px));
+ font-weight: 600;
+
+ .author-name {
+ color: #333;
+ }
+ }
+
+ .todo-body {
+ margin-right: 174px;
+
+ .todo-note {
+ word-wrap: break-word;
+
+ .md {
+ color: #7f8fa4;
+ font-size: $gl-font-size;
+
+ p {
+ color: #5c5d5e;
+ }
+ }
+
+ pre {
+ border: none;
+ background: #f9f9f9;
+ border-radius: 0;
+ color: #777;
+ margin: 0 20px;
+ overflow: hidden;
+ }
+
+ .note-image-attach {
+ margin-top: 4px;
+ margin-left: 0;
+ max-width: 200px;
+ float: none;
+ }
+
+ p:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+}
+
+@media (max-width: $screen-xs-max) {
+ .todo-item {
+ padding-left: $gl-padding;
+
+ .todo-title {
+ white-space: normal;
+ overflow: visible;
+ max-width: 100%;
+ }
+
+ .avatar {
+ display: none;
+ }
+
+ .todo-body {
+ margin: 0;
+ border-left: 2px solid #ddd;
+ padding-left: 10px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index d4ab6967ccd..73c7c9f687c 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -1,18 +1,27 @@
.tree-holder {
+ > .nav-block {
+ margin: 11px 0;
+ }
+
+ .file-finder {
+ width: 50%;
+ .file-finder-input {
+ width: 95%;
+ display: inline-block;
+ }
+ }
.tree-table {
margin-bottom: 0;
tr {
> td, > th {
- line-height: 28px;
+ line-height: 26px;
}
&:hover {
td {
- background: $hover;
- border-top: 1px solid #ADF;
- border-bottom: 1px solid #ADF;
+ background: $row-hover;
}
cursor: pointer;
}
@@ -37,7 +46,7 @@
img {
position: relative;
- top:-1px;
+ top: -1px;
}
}
@@ -78,12 +87,14 @@
.blob-commit-info {
list-style: none;
+ padding: $gl-padding;
+ background: $background-color;
+ border: 1px solid $border-color;
+ border-bottom: none;
margin: 0;
- padding: 0;
- margin-bottom: 5px;
.commit {
- padding: $gl-padding 0;
+ padding: 0;
.commit-row-title {
.commit-row-message {
@@ -107,3 +118,8 @@
font-weight: normal;
color: $md-link-color;
}
+
+.tree-controls {
+ float: right;
+ margin-top: 11px;
+}
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index 185f3622e64..587bd6a1e8a 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -3,4 +3,15 @@
margin: 35px 0 20px;
font-weight: bold;
}
+
+ .example {
+ &:before {
+ content: "Example";
+ color: #bbb;
+ }
+
+ padding: 15px;
+ border: 1px dashed #ddd;
+ margin-bottom: 15px;
+ }
}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index cdf514197cb..dfaeba41cf6 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -4,8 +4,3 @@
margin-right: auto;
padding-right: 7px;
}
-
-.wiki-last-edit-by {
- font-size: 80%;
- font-weight: normal;
-}
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 9a50096c0d0..8886c1dff56 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -2,23 +2,23 @@
// color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg
// see also: https://gist.github.com/jasonm23/2868981
- $black: #000000;
+ $black: #000;
$red: #cd0000;
$green: #00cd00;
$yellow: #cdcd00;
- $blue: #0000ee; // according to wikipedia, this is the xterm standard
+ $blue: #00e; // according to wikipedia, this is the xterm standard
//$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile)
$magenta: #cd00cd;
$cyan: #00cdcd;
$white: #e5e5e5;
$l-black: #7f7f7f;
- $l-red: #ff0000;
- $l-green: #00ff00;
- $l-yellow: #ffff00;
+ $l-red: #f00;
+ $l-green: #0f0;
+ $l-yellow: #ff0;
$l-blue: #5c5cff;
- $l-magenta: #ff00ff;
- $l-cyan: #00ffff;
- $l-white: #ffffff;
+ $l-magenta: #f0f;
+ $l-cyan: #0ff;
+ $l-white: #fff;
.term-bold {
font-weight: bold;
@@ -136,7 +136,7 @@
.xterm-fg-0 {
- color: #000000;
+ color: #000;
}
.xterm-fg-1 {
color: #800000;
@@ -163,28 +163,28 @@
color: #808080;
}
.xterm-fg-9 {
- color: #ff0000;
+ color: #f00;
}
.xterm-fg-10 {
- color: #00ff00;
+ color: #0f0;
}
.xterm-fg-11 {
- color: #ffff00;
+ color: #ff0;
}
.xterm-fg-12 {
- color: #0000ff;
+ color: #00f;
}
.xterm-fg-13 {
- color: #ff00ff;
+ color: #f0f;
}
.xterm-fg-14 {
- color: #00ffff;
+ color: #0ff;
}
.xterm-fg-15 {
- color: #ffffff;
+ color: #fff;
}
.xterm-fg-16 {
- color: #000000;
+ color: #000;
}
.xterm-fg-17 {
color: #00005f;
@@ -199,7 +199,7 @@
color: #0000d7;
}
.xterm-fg-21 {
- color: #0000ff;
+ color: #00f;
}
.xterm-fg-22 {
color: #005f00;
@@ -274,7 +274,7 @@
color: #00d7ff;
}
.xterm-fg-46 {
- color: #00ff00;
+ color: #0f0;
}
.xterm-fg-47 {
color: #00ff5f;
@@ -289,7 +289,7 @@
color: #00ffd7;
}
.xterm-fg-51 {
- color: #00ffff;
+ color: #0ff;
}
.xterm-fg-52 {
color: #5f0000;
@@ -724,7 +724,7 @@
color: #d7ffff;
}
.xterm-fg-196 {
- color: #ff0000;
+ color: #f00;
}
.xterm-fg-197 {
color: #ff005f;
@@ -739,7 +739,7 @@
color: #ff00d7;
}
.xterm-fg-201 {
- color: #ff00ff;
+ color: #f0f;
}
.xterm-fg-202 {
color: #ff5f00;
@@ -814,7 +814,7 @@
color: #ffd7ff;
}
.xterm-fg-226 {
- color: #ffff00;
+ color: #ff0;
}
.xterm-fg-227 {
color: #ffff5f;
@@ -829,7 +829,7 @@
color: #ffffd7;
}
.xterm-fg-231 {
- color: #ffffff;
+ color: #fff;
}
.xterm-fg-232 {
color: #080808;
@@ -850,7 +850,7 @@
color: #3a3a3a;
}
.xterm-fg-238 {
- color: #444444;
+ color: #444;
}
.xterm-fg-239 {
color: #4e4e4e;
@@ -901,6 +901,6 @@
color: #e4e4e4;
}
.xterm-fg-255 {
- color: #eeeeee;
+ color: #eee;
}
}
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index 20bc5173f1d..2eac0cabf7a 100644
--- a/app/controllers/abuse_reports_controller.rb
+++ b/app/controllers/abuse_reports_controller.rb
@@ -2,6 +2,7 @@ class AbuseReportsController < ApplicationController
def new
@abuse_report = AbuseReport.new
@abuse_report.user_id = params[:user_id]
+ @ref_url = params.fetch(:ref_url, '')
end
def create
@@ -9,12 +10,10 @@ class AbuseReportsController < ApplicationController
@abuse_report.reporter = current_user
if @abuse_report.save
- if current_application_settings.admin_notification_email.present?
- AbuseReportMailer.notify(@abuse_report.id).deliver_later
- end
+ @abuse_report.notify
message = "Thank you for your report. A GitLab administrator will look into it shortly."
- redirect_to root_path, notice: message
+ redirect_to @abuse_report.user, notice: message
else
render :new
end
@@ -23,6 +22,9 @@ class AbuseReportsController < ApplicationController
private
def report_params
- params.require(:abuse_report).permit(:user_id, :message)
+ params.require(:abuse_report).permit(%i(
+ message
+ user_id
+ ))
end
end
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index 38a5a9fca08..e9b0972bdd8 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -6,11 +6,9 @@ class Admin::AbuseReportsController < Admin::ApplicationController
def destroy
abuse_report = AbuseReport.find(params[:id])
- if params[:remove_user]
- abuse_report.user.destroy
- end
-
+ abuse_report.remove_user(deleted_by: current_user) if params[:remove_user]
abuse_report.destroy
+
render nothing: true
end
end
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
new file mode 100644
index 00000000000..26cf74e4849
--- /dev/null
+++ b/app/controllers/admin/appearances_controller.rb
@@ -0,0 +1,57 @@
+class Admin::AppearancesController < Admin::ApplicationController
+ before_action :set_appearance, except: :create
+
+ def show
+ end
+
+ def preview
+ end
+
+ def create
+ @appearance = Appearance.new(appearance_params)
+
+ if @appearance.save
+ redirect_to admin_appearances_path, notice: 'Appearance was successfully created.'
+ else
+ render action: 'show'
+ end
+ end
+
+ def update
+ if @appearance.update(appearance_params)
+ redirect_to admin_appearances_path, notice: 'Appearance was successfully updated.'
+ else
+ render action: 'show'
+ end
+ end
+
+ def logo
+ @appearance.remove_logo!
+
+ @appearance.save
+
+ redirect_to admin_appearances_path, notice: 'Logo was succesfully removed.'
+ end
+
+ def header_logos
+ @appearance.remove_header_logo!
+ @appearance.save
+
+ redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.'
+ end
+
+ private
+
+ # Use callbacks to share common setup or constraints between actions.
+ def set_appearance
+ @appearance = Appearance.last || Appearance.new
+ end
+
+ # Only allow a trusted parameter "white list" through.
+ def appearance_params
+ params.require(:appearance).permit(
+ :title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache,
+ :updated_by
+ )
+ end
+end
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 10e736fd362..04a99d8c84a 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -70,14 +70,18 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:metrics_enabled,
:metrics_host,
:metrics_port,
- :metrics_username,
- :metrics_password,
:metrics_pool_size,
:metrics_timeout,
:metrics_method_call_threshold,
+ :metrics_sample_interval,
:recaptcha_enabled,
:recaptcha_site_key,
:recaptcha_private_key,
+ :sentry_enabled,
+ :sentry_dsn,
+ :akismet_enabled,
+ :akismet_api_key,
+ :email_author_in_body,
restricted_visibility_levels: [],
import_sources: []
)
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 497c34f8f49..fc342924987 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -1,8 +1,12 @@
class Admin::BroadcastMessagesController < Admin::ApplicationController
- before_action :broadcast_messages
+ before_action :finder, only: [:edit, :update, :destroy]
def index
- @broadcast_message = BroadcastMessage.new
+ @broadcast_messages = BroadcastMessage.reorder("ends_at DESC").page(params[:page])
+ @broadcast_message = BroadcastMessage.new
+ end
+
+ def edit
end
def create
@@ -15,8 +19,16 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end
end
+ def update
+ if @broadcast_message.update(broadcast_message_params)
+ redirect_to admin_broadcast_messages_path, notice: 'Broadcast Message was successfully updated.'
+ else
+ render :edit
+ end
+ end
+
def destroy
- BroadcastMessage.find(params[:id]).destroy
+ @broadcast_message.destroy
respond_to do |format|
format.html { redirect_back_or_default(default: { action: 'index' }) }
@@ -24,16 +36,23 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end
end
+ def preview
+ @message = broadcast_message_params[:message]
+ end
+
protected
- def broadcast_messages
- @broadcast_messages ||= BroadcastMessage.order("starts_at DESC").page(params[:page])
+ def finder
+ @broadcast_message = BroadcastMessage.find(params[:id])
end
def broadcast_message_params
- params.require(:broadcast_message).permit(
- :alert_type, :color, :ends_at, :font,
- :message, :starts_at
- )
+ params.require(:broadcast_message).permit(%i(
+ color
+ ends_at
+ font
+ message
+ starts_at
+ ))
end
end
diff --git a/app/controllers/admin/builds_controller.rb b/app/controllers/admin/builds_controller.rb
index 83d9684c706..0db91eaaf2e 100644
--- a/app/controllers/admin/builds_controller.rb
+++ b/app/controllers/admin/builds_controller.rb
@@ -5,12 +5,12 @@ class Admin::BuildsController < Admin::ApplicationController
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
- when 'all'
- @builds
+ when 'running'
+ @builds.running_or_pending.reverse_order
when 'finished'
@builds.finished
else
- @builds.running_or_pending.reverse_order
+ @builds
end
@builds = @builds.page(params[:page]).per(30)
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 4d3e48f7f81..668396a0f20 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -55,7 +55,7 @@ class Admin::GroupsController < Admin::ApplicationController
private
def group
- @group = Group.find_by(path: params[:id])
+ @group ||= Group.find_by(path: params[:id])
end
def group_params
diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb
index e383fe38ea6..79a53556f0a 100644
--- a/app/controllers/admin/identities_controller.rb
+++ b/app/controllers/admin/identities_controller.rb
@@ -26,6 +26,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
def update
if @identity.update_attributes(identity_params)
+ RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.'
else
render :edit
@@ -34,6 +35,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
def destroy
if @identity.destroy
+ RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully removed.'
else
redirect_to admin_user_identities_path(@user), alert: 'Failed to remove user identity.'
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index 3b070e65d0d..d79ce2b10fe 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -53,6 +53,6 @@ class Admin::LabelsController < Admin::ApplicationController
end
def label_params
- params[:label].permit(:title, :color)
+ params[:label].permit(:title, :description, :color)
end
end
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
new file mode 100644
index 00000000000..377e9741e5f
--- /dev/null
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -0,0 +1,17 @@
+class Admin::SpamLogsController < Admin::ApplicationController
+ def index
+ @spam_logs = SpamLog.order(id: :desc).page(params[:page])
+ end
+
+ def destroy
+ spam_log = SpamLog.find(params[:id])
+
+ if params[:remove_user]
+ spam_log.remove_user
+ redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed."
+ else
+ spam_log.destroy
+ render nothing: true
+ end
+ end
+end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index d7c927d444c..9abf08d0e19 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -40,7 +40,9 @@ class Admin::UsersController < Admin::ApplicationController
end
def unblock
- if user.activate
+ if user.ldap_blocked?
+ redirect_back_or_admin_user(alert: "This user cannot be unlocked manually from GitLab")
+ elsif user.activate
redirect_back_or_admin_user(notice: "Successfully unblocked")
else
redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked")
@@ -117,10 +119,10 @@ class Admin::UsersController < Admin::ApplicationController
end
def destroy
- DeleteUserService.new(current_user).execute(user)
+ DeleteUserWorker.perform_async(current_user.id, user.id)
respond_to do |format|
- format.html { redirect_to admin_users_path }
+ format.html { redirect_to admin_users_path, notice: "The user is being deleted." }
format.json { head :ok }
end
end
@@ -148,7 +150,7 @@ class Admin::UsersController < Admin::ApplicationController
:email, :remember_me, :bio, :name, :username,
:skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password,
:extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password,
- :projects_limit, :can_create_group, :admin, :key_id
+ :projects_limit, :can_create_group, :admin, :key_id, :external
)
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d9a37a4d45f..1f55b18e0b1 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base
before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check
+ before_action :sentry_user_context
before_action :default_headers
before_action :add_gon_variables
before_action :configure_permitted_parameters, if: :devise_controller?
@@ -24,6 +25,7 @@ class ApplicationController < ActionController::Base
helper_method :abilities, :can?, :current_application_settings
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?
+ helper_method :repository, :can_collaborate_with_project?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -41,6 +43,16 @@ class ApplicationController < ActionController::Base
protected
+ def sentry_user_context
+ if Rails.env.production? && current_application_settings.sentry_enabled && current_user
+ Raven.user_context(
+ id: current_user.id,
+ email: current_user.email,
+ username: current_user.username,
+ )
+ end
+ end
+
# From https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example
# https://gist.github.com/josevalim/fb706b1e933ef01e4fb6
def authenticate_user_from_token!
@@ -48,6 +60,8 @@ class ApplicationController < ActionController::Base
params[:authenticity_token].presence
elsif params[:private_token].presence
params[:private_token].presence
+ elsif request.headers['PRIVATE-TOKEN'].present?
+ request.headers['PRIVATE-TOKEN']
end
user = user_token && User.find_by_authentication_token(user_token.to_s)
@@ -115,7 +129,7 @@ class ApplicationController < ActionController::Base
# localhost/group/project
#
if id =~ /\.git\Z/
- redirect_to request.original_url.gsub(/\.git\Z/, '') and return
+ redirect_to request.original_url.gsub(/\.git\/?\Z/, '') and return
end
project_path = "#{namespace}/#{id}"
@@ -150,7 +164,7 @@ class ApplicationController < ActionController::Base
end
def git_not_found!
- render html: "errors/git_not_found", layout: "errors", status: 404
+ render "errors/git_not_found.html", layout: "errors", status: 404
end
def method_missing(method_sym, *arguments, &block)
@@ -232,6 +246,8 @@ class ApplicationController < ActionController::Base
def ldap_security_check
if current_user && current_user.requires_ldap_check?
+ return unless current_user.try_obtain_ldap_lease
+
unless Gitlab::LDAP::Access.allowed?(current_user)
sign_out current_user
flash[:alert] = "Access denied for your LDAP account."
@@ -263,9 +279,10 @@ class ApplicationController < ActionController::Base
}
end
- def view_to_html_string(partial)
+ def view_to_html_string(partial, locals = {})
render_to_string(
partial,
+ locals: locals,
layout: false,
formats: [:html]
)
@@ -286,7 +303,8 @@ class ApplicationController < ActionController::Base
end
def set_filters_params
- params[:sort] ||= 'created_desc'
+ set_default_sort
+
params[:scope] = 'all' if params[:scope].blank?
params[:state] = 'opened' if params[:state].blank?
@@ -393,4 +411,31 @@ class ApplicationController < ActionController::Base
current_user.nil? && root_path == request.path
end
+
+ def can_collaborate_with_project?(project = nil)
+ project ||= @project
+
+ can?(current_user, :push_code, project) ||
+ (current_user && current_user.already_forked?(project))
+ end
+
+ private
+
+ def set_default_sort
+ key = if is_a_listing_page_for?('issues') || is_a_listing_page_for?('merge_requests')
+ 'issuable_sort'
+ end
+
+ cookies[key] = params[:sort] if key && params[:sort].present?
+ params[:sort] = cookies[key] if key
+ params[:sort] ||= 'id_desc'
+ end
+
+ def is_a_listing_page_for?(page_type)
+ controller_name, action_name = params.values_at(:controller, :action)
+
+ (controller_name == "projects/#{page_type}" && action_name == 'index') ||
+ (controller_name == 'groups' && action_name == page_type) ||
+ (controller_name == 'dashboard' && action_name == page_type)
+ end
end
diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb
index c420b59c3a2..5bb7d499cdc 100644
--- a/app/controllers/ci/application_controller.rb
+++ b/app/controllers/ci/application_controller.rb
@@ -3,52 +3,5 @@ module Ci
def self.railtie_helpers_paths
"app/helpers/ci"
end
-
- private
-
- def authorize_access_project!
- unless can?(current_user, :read_project, project)
- return page_404
- end
- end
-
- def authorize_manage_builds!
- unless can?(current_user, :manage_builds, project)
- return page_404
- end
- end
-
- def authenticate_admin!
- return render_404 unless current_user.is_admin?
- end
-
- def authorize_manage_project!
- unless can?(current_user, :admin_project, project)
- return page_404
- end
- end
-
- def page_404
- render file: "#{Rails.root}/public/404.html", status: 404, layout: false
- end
-
- def default_headers
- headers['X-Frame-Options'] = 'DENY'
- headers['X-XSS-Protection'] = '1; mode=block'
- end
-
- # JSON for infinite scroll via Pager object
- def pager_json(partial, count)
- html = render_to_string(
- partial,
- layout: false,
- formats: [:html]
- )
-
- render json: {
- html: html,
- count: count
- }
- end
end
end
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index e782a51e7eb..a7af3cb8345 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -6,11 +6,13 @@ module Ci
end
def create
- if params[:content].blank?
+ @content = params[:content]
+
+ if @content.blank?
@status = false
@error = "Please provide content of .gitlab-ci.yml"
else
- @config_processor = Ci::GitlabCiYamlProcessor.new params[:content]
+ @config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
@status = true
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
index 3004c2d27f0..081e01a75e0 100644
--- a/app/controllers/ci/projects_controller.rb
+++ b/app/controllers/ci/projects_controller.rb
@@ -1,9 +1,9 @@
module Ci
class ProjectsController < Ci::ApplicationController
- before_action :project, except: [:index]
- before_action :authenticate_user!, except: [:index, :build, :badge]
- before_action :authorize_access_project!, except: [:index, :badge]
+ before_action :project
+ before_action :authorize_read_project!, except: [:badge]
before_action :no_cache, only: [:badge]
+ skip_before_action :authenticate_user!, only: [:badge]
protect_from_forgery
def show
@@ -13,9 +13,14 @@ module Ci
# Project status badge
# Image with build status for sha or ref
+ #
+ # This action in DEPRECATED, this is here only for backwards compatibility
+ # with projects migrated from GitLab CI.
+ #
def badge
- image = Ci::ImageForBuildService.new.execute(@project, params)
+ return render_404 unless @project
+ image = Ci::ImageForBuildService.new.execute(@project, params)
send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml"
end
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
new file mode 100644
index 00000000000..0a995c45bdf
--- /dev/null
+++ b/app/controllers/concerns/continue_params.rb
@@ -0,0 +1,13 @@
+module ContinueParams
+ extend ActiveSupport::Concern
+
+ def continue_params
+ continue_params = params[:continue]
+ return nil unless continue_params
+
+ continue_params = continue_params.permit(:to, :notice, :notice_now)
+ return unless continue_params[:to] && continue_params[:to].start_with?('/')
+
+ continue_params
+ end
+end
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 62127a09081..787416c17ab 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -13,17 +13,11 @@ module CreatesCommit
result = service.new(@tree_edit_project, current_user, commit_params).execute
if result[:status] == :success
- flash[:notice] = success_notice || "Your changes have been successfully committed."
-
- if create_merge_request?
- success_path = new_merge_request_path
- target = different_project? ? "project" : "branch"
- flash[:notice] << " You can now submit a merge request to get this change into the original #{target}."
- end
+ update_flash_notice(success_notice)
respond_to do |format|
- format.html { redirect_to success_path }
- format.json { render json: { message: "success", filePath: success_path } }
+ format.html { redirect_to final_success_path(success_path) }
+ format.json { render json: { message: "success", filePath: final_success_path(success_path) } }
end
else
flash[:alert] = result[:message]
@@ -41,14 +35,32 @@ module CreatesCommit
end
def authorize_edit_tree!
- return if can?(current_user, :push_code, project)
- return if current_user && current_user.already_forked?(project)
+ return if can_collaborate_with_project?
access_denied!
end
private
+ def update_flash_notice(success_notice)
+ flash[:notice] = success_notice || "Your changes have been successfully committed."
+
+ if create_merge_request?
+ if merge_request_exists?
+ flash[:notice] = nil
+ else
+ target = different_project? ? "project" : "branch"
+ flash[:notice] << " You can now submit a merge request to get this change into the original #{target}."
+ end
+ end
+ end
+
+ def final_success_path(success_path)
+ return success_path unless create_merge_request?
+
+ merge_request_exists? ? existing_merge_request_path : new_merge_request_path
+ end
+
def new_merge_request_path
new_namespace_project_merge_request_path(
@mr_source_project.namespace,
@@ -62,6 +74,19 @@ module CreatesCommit
)
end
+ def existing_merge_request_path
+ namespace_project_merge_request_path(@mr_target_project.namespace, @mr_target_project, @merge_request)
+ end
+
+ def merge_request_exists?
+ return @merge_request if defined?(@merge_request)
+
+ @merge_request = @mr_target_project.merge_requests.opened.find_by(
+ source_branch: @mr_source_branch,
+ target_branch: @mr_target_branch
+ )
+ end
+
def different_project?
@mr_source_project != @mr_target_project
end
@@ -75,7 +100,7 @@ module CreatesCommit
end
def set_commit_variables
- @mr_source_branch = @target_branch
+ @mr_source_branch ||= @target_branch
if can?(current_user, :push_code, @project)
# Edit file in this project
@@ -89,7 +114,7 @@ module CreatesCommit
else
# Merge request to this project
@mr_target_project = @project
- @mr_target_branch = @ref
+ @mr_target_branch ||= @ref
end
else
# Edit file in fork
@@ -97,7 +122,7 @@ module CreatesCommit
# Merge request from fork to this project
@mr_source_project = @tree_edit_project
@mr_target_project = @project
- @mr_target_branch = @mr_target_project.repository.root_ref
+ @mr_target_branch ||= @ref
end
end
end
diff --git a/app/controllers/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb
new file mode 100644
index 00000000000..f63b703d101
--- /dev/null
+++ b/app/controllers/concerns/filter_projects.rb
@@ -0,0 +1,15 @@
+# == FilterProjects
+#
+# Controller concern to handle projects filtering
+# * by name
+# * by archived state
+#
+module FilterProjects
+ extend ActiveSupport::Concern
+
+ def filter_projects(projects)
+ projects = projects.search(params[:filter_projects]) if params[:filter_projects].present?
+ projects = projects.non_archived if params[:archived].blank?
+ projects
+ end
+end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index effd4721949..ef8e74a4641 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -2,10 +2,12 @@ module IssuesAction
extend ActiveSupport::Concern
def issues
- @issues = get_issues_collection
+ @issues = get_issues_collection.non_archived
@issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE)
@issues = @issues.preload(:author, :project)
+ @label = @issuable_finder.labels.first
+
respond_to do |format|
format.html
format.atom { render layout: false }
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index f7a25111db9..9c49596bd0b 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -2,8 +2,10 @@ module MergeRequestsAction
extend ActiveSupport::Concern
def merge_requests
- @merge_requests = get_merge_requests_collection
+ @merge_requests = get_merge_requests_collection.non_archived
@merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE)
@merge_requests = @merge_requests.preload(:author, :target_project)
+
+ @label = @issuable_finder.labels.first
end
end
diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb
new file mode 100644
index 00000000000..8a43c0b93c4
--- /dev/null
+++ b/app/controllers/concerns/toggle_subscription_action.rb
@@ -0,0 +1,17 @@
+module ToggleSubscriptionAction
+ extend ActiveSupport::Concern
+
+ def toggle_subscription
+ return unless current_user
+
+ subscribable_resource.toggle_subscription(current_user)
+
+ render nothing: true
+ end
+
+ private
+
+ def subscribable_resource
+ raise NotImplementedError
+ end
+end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 58e9049f158..0e8b63872ca 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -1,9 +1,15 @@
class Dashboard::ProjectsController < Dashboard::ApplicationController
+ include FilterProjects
+
before_action :event_filter
def index
- @projects = current_user.authorized_projects.sorted_by_activity.non_archived
+ @projects = current_user.authorized_projects.sorted_by_activity
+ @projects = filter_projects(@projects)
@projects = @projects.includes(:namespace)
+ @projects = @projects.sort(@sort = params[:sort])
+ @projects = @projects.page(params[:page]).per(PER_PAGE)
+
@last_push = current_user.recent_push
respond_to do |format|
@@ -13,13 +19,21 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
load_events
render layout: false
end
+ format.json do
+ render json: {
+ html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ }
+ end
end
end
def starred
- @projects = current_user.starred_projects
+ @projects = current_user.starred_projects.sorted_by_activity
+ @projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
+ @projects = @projects.page(params[:page]).per(PER_PAGE)
+
@last_push = current_user.recent_push
@groups = []
@@ -27,8 +41,9 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
format.html
format.json do
- load_events
- pager_json("events/_events", @events.count)
+ render json: {
+ html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ }
end
end
end
@@ -36,7 +51,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
def load_events
- @events = Event.in_projects(@projects.pluck(:id))
+ @events = Event.in_projects(@projects)
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
new file mode 100644
index 00000000000..7857af9c5de
--- /dev/null
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -0,0 +1,44 @@
+class Dashboard::TodosController < Dashboard::ApplicationController
+ before_action :find_todos, only: [:index, :destroy, :destroy_all]
+
+ def index
+ @todos = @todos.page(params[:page]).per(PER_PAGE)
+ end
+
+ def destroy
+ todo.done!
+
+ todo_notice = 'Todo was successfully marked as done.'
+
+ respond_to do |format|
+ format.html { redirect_to dashboard_todos_path, notice: todo_notice }
+ format.js { render nothing: true }
+ format.json do
+ render json: { count: @todos.size, done_count: current_user.todos.done.count }
+ end
+ end
+ end
+
+ def destroy_all
+ @todos.each(&:done!)
+
+ respond_to do |format|
+ format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
+ format.js { render nothing: true }
+ format.json do
+ find_todos
+ render json: { count: @todos.size, done_count: current_user.todos.done.count }
+ end
+ end
+ end
+
+ private
+
+ def todo
+ @todo ||= current_user.todos.find(params[:id])
+ end
+
+ def find_todos
+ @todos = TodosFinder.new(current_user, params).execute
+ end
+end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 087da935087..139e40db180 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -23,14 +23,14 @@ class DashboardController < Dashboard::ApplicationController
protected
def load_events
- project_ids =
+ projects =
if params[:filter] == "starred"
current_user.starred_projects
else
current_user.authorized_projects
- end.pluck(:id)
+ end
- @events = Event.in_projects(project_ids)
+ @events = Event.in_projects(projects)
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb
new file mode 100644
index 00000000000..1bec5a7d27f
--- /dev/null
+++ b/app/controllers/emojis_controller.rb
@@ -0,0 +1,6 @@
+class EmojisController < ApplicationController
+ layout false
+
+ def index
+ end
+end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index 9575a87ee41..a9bf4321f73 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,6 +1,6 @@
class Explore::GroupsController < Explore::ApplicationController
def index
- @groups = GroupsFinder.new.execute(current_user)
+ @groups = Group.order_id_desc
@groups = @groups.search(params[:search]) if params[:search].present?
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page]).per(PER_PAGE)
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index a5aeaed66c5..8271ca87436 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -1,24 +1,53 @@
class Explore::ProjectsController < Explore::ApplicationController
+ include FilterProjects
+
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.non_archived
- @projects = @projects.search(params[:search]) if params[:search].present?
+ @projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ }
+ end
+ end
end
def trending
- @trending_projects = TrendingProjectsFinder.new.execute(current_user)
- @trending_projects = @trending_projects.non_archived
- @trending_projects = @trending_projects.page(params[:page]).per(PER_PAGE)
+ @projects = TrendingProjectsFinder.new.execute(current_user)
+ @projects = filter_projects(@projects)
+ @projects = @projects.page(params[:page]).per(PER_PAGE)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ }
+ end
+ end
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(PER_PAGE)
+ @projects = ProjectsFinder.new.execute(current_user)
+ @projects = filter_projects(@projects)
+ @projects = @projects.reorder('star_count DESC')
+ @projects = @projects.page(params[:page]).per(PER_PAGE)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ }
+ end
+ end
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index fb26a4e6fc3..06c5c8be9a5 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -1,19 +1,21 @@
class GroupsController < Groups::ApplicationController
+ include FilterProjects
include IssuesAction
include MergeRequestsAction
- skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests]
respond_to :html
- before_action :group, except: [:new, :create]
+
+ skip_before_action :authenticate_user!, only: [:index, :show, :issues, :merge_requests]
+ before_action :group, except: [:index, :new, :create]
# Authorize
- before_action :authorize_read_group!, except: [:show, :new, :create, :autocomplete]
+ before_action :authorize_read_group!, except: [:index, :show, :new, :create, :autocomplete]
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
before_action :authorize_create_group!, only: [:new, :create]
# Load group projects
- before_action :load_projects, except: [:new, :create, :projects, :edit, :update, :autocomplete]
- before_action :event_filter, only: :show
+ before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete]
+ before_action :event_filter, only: [:activity]
layout :determine_layout
@@ -40,13 +42,19 @@ class GroupsController < Groups::ApplicationController
def show
@last_push = current_user.recent_push if current_user
@projects = @projects.includes(:namespace)
+ @projects = filter_projects(@projects)
+ @projects = @projects.sort(@sort = params[:sort])
+ @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+
+ @shared_projects = @group.shared_projects
respond_to do |format|
format.html
format.json do
- load_events
- pager_json("events/_events", @events.count)
+ render json: {
+ html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ }
end
format.atom do
@@ -56,6 +64,17 @@ class GroupsController < Groups::ApplicationController
end
end
+ def activity
+ respond_to do |format|
+ format.html
+
+ format.json do
+ load_events
+ pager_json("events/_events", @events.count)
+ end
+ end
+ end
+
def edit
end
@@ -81,14 +100,11 @@ class GroupsController < Groups::ApplicationController
def group
@group ||= Group.find_by(path: params[:id])
+ @group || render_404
end
def load_projects
- @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity.non_archived
- end
-
- def project_ids
- @projects.pluck(:id)
+ @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity
end
# Dont allow unauthorized access to group
@@ -119,11 +135,11 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar, :public)
+ params.require(:group).permit(:name, :description, :path, :avatar, :public, :share_with_group_lock)
end
def load_events
- @events = Event.in_projects(project_ids)
+ @events = Event.in_projects(@projects)
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index dc22101cd5e..d1e4ac10f6c 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -8,7 +8,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
layout 'profile'
def index
- head :forbidden and return
+ set_index_vars
end
def create
@@ -20,18 +20,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to oauth_application_url(@application)
else
- render :new
+ set_index_vars
+ render :index
end
end
- def destroy
- if @application.destroy
- flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :destroy])
- end
-
- redirect_to applications_profile_url
- end
-
private
def verify_user_oauth_applications_enabled
@@ -40,6 +33,17 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
redirect_to applications_profile_url
end
+ def set_index_vars
+ @applications = current_user.oauth_applications
+ @authorized_tokens = current_user.oauth_authorized_tokens
+ @authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
+ @authorized_apps = @authorized_tokens.map(&:application).uniq.reject(&:nil?)
+
+ # Don't overwrite a value possibly set by `create`
+ @application ||= Doorkeeper::Application.new
+ end
+
+ # Override Doorkeeper to scope to the current user
def set_application
@application = current_user.oauth_applications.find(params[:id])
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 4cad98b8e98..21135f7d607 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -1,4 +1,5 @@
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
+ include AuthenticatesWithTwoFactor
protect_from_forgery except: [:kerberos, :saml, :cas3]
@@ -21,21 +22,46 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# We only find ourselves here
# if the authentication to LDAP was successful.
def ldap
- @user = Gitlab::LDAP::User.new(oauth)
- @user.save if @user.changed? # will also save new users
- gl_user = @user.gl_user
- gl_user.remember_me = params[:remember_me] if @user.persisted?
+ ldap_user = Gitlab::LDAP::User.new(oauth)
+ ldap_user.save if ldap_user.changed? # will also save new users
+
+ @user = ldap_user.gl_user
+ @user.remember_me = params[:remember_me] if ldap_user.persisted?
# Do additional LDAP checks for the user filter and EE features
- if @user.allowed?
- log_audit_event(gl_user, with: :ldap)
- sign_in_and_redirect(gl_user)
+ if ldap_user.allowed?
+ if @user.two_factor_enabled?
+ prompt_for_two_factor(@user)
+ else
+ log_audit_event(@user, with: :ldap)
+ sign_in_and_redirect(@user)
+ end
else
flash[:alert] = "Access denied for your LDAP account."
redirect_to new_user_session_path
end
end
+ def saml
+ if current_user
+ log_audit_event(current_user, with: :saml)
+ # Update SAML identity if data has changed.
+ identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml)
+ if identity.nil?
+ current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
+ redirect_to profile_account_path, notice: 'Authentication method updated'
+ else
+ redirect_to after_sign_in_path_for(current_user)
+ end
+ else
+ saml_user = Gitlab::Saml::User.new(oauth)
+ saml_user.save
+ @user = saml_user.gl_user
+
+ continue_login_process
+ end
+ end
+
def omniauth_error
@provider = params[:provider]
@error = params[:error]
@@ -59,25 +85,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
- @user = Gitlab::OAuth::User.new(oauth)
- @user.save
+ oauth_user = Gitlab::OAuth::User.new(oauth)
+ oauth_user.save
+ @user = oauth_user.gl_user
- # Only allow properly saved users to login.
- if @user.persisted? && @user.valid?
- log_audit_event(@user.gl_user, with: oauth['provider'])
- sign_in_and_redirect(@user.gl_user)
- else
- error_message =
- if @user.gl_user.errors.any?
- @user.gl_user.errors.map do |attribute, message|
- "#{attribute} #{message}"
- end.join(", ")
- else
- ''
- end
-
- redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return
- end
+ continue_login_process
end
rescue Gitlab::OAuth::SignupDisabledError
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
@@ -98,6 +110,18 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
session[:service_tickets][provider] = ticket
end
+ def continue_login_process
+ # Only allow properly saved users to login.
+ if @user.persisted? && @user.valid?
+ log_audit_event(@user, with: oauth['provider'])
+ sign_in_and_redirect(@user)
+ else
+ error_message = @user.errors.full_messages.to_sentence
+
+ redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return
+ end
+ end
+
def oauth
@oauth ||= request.env['omniauth.auth']
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index f74daff3bd0..a8575e037e4 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -23,6 +23,14 @@ class PasswordsController < Devise::PasswordsController
end
end
+ def update
+ super do |resource|
+ if resource.valid? && resource.require_password?
+ resource.update_attribute(:password_automatically_set, false)
+ end
+ end
+ end
+
protected
def resource_from_email
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index f3224148fda..b88c080352b 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -3,23 +3,21 @@ class Profiles::KeysController < Profiles::ApplicationController
def index
@keys = current_user.keys
+ @key = Key.new
end
def show
@key = current_user.keys.find(params[:id])
end
- def new
- @key = current_user.keys.new
- end
-
def create
@key = current_user.keys.new(key_params)
if @key.save
redirect_to profile_key_path(@key)
else
- render 'new'
+ @keys = current_user.keys.select(&:persisted?)
+ render :index
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 6e91d9b4ad9..8f83fdd02bc 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -12,11 +12,13 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.save! if current_user.changed?
- if two_factor_grace_period_expired?
- flash.now[:alert] = 'You must configure Two-Factor Authentication in your account.'
- else
- grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
- flash.now[:alert] = "You must configure Two-Factor Authentication in your account until #{l(grace_period_deadline)}."
+ if two_factor_authentication_required?
+ if two_factor_grace_period_expired?
+ flash.now[:alert] = 'You must enable Two-factor Authentication for your account.'
+ else
+ grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
+ flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}."
+ end
end
@qr_code = build_qr_code
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 28803164fcf..32fca6b838e 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -8,13 +8,6 @@ class ProfilesController < Profiles::ApplicationController
def show
end
- def applications
- @applications = current_user.oauth_applications
- @authorized_tokens = current_user.oauth_authorized_tokens
- @authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
- @authorized_apps = @authorized_tokens.map(&:application).uniq - [nil]
- end
-
def update
user_params.except!(:email) if @user.ldap_user?
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index dd32d509191..a326bc58215 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -28,6 +28,11 @@ class Projects::ApplicationController < ApplicationController
private
+ def apply_diff_view_cookie!
+ view = params[:view] || cookies[:diff_view]
+ cookies.permanent[:diff_view] = params[:view] = view if view
+ end
+
def builds_enabled
return render_404 unless @project.builds_enabled?
end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
new file mode 100644
index 00000000000..cfea1266516
--- /dev/null
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -0,0 +1,46 @@
+class Projects::ArtifactsController < Projects::ApplicationController
+ layout 'project'
+ before_action :authorize_read_build!
+
+ def download
+ unless artifacts_file.file_storage?
+ return redirect_to artifacts_file.url
+ end
+
+ unless artifacts_file.exists?
+ return render_404
+ end
+
+ send_file artifacts_file.path, disposition: 'attachment'
+ end
+
+ def browse
+ return render_404 unless build.artifacts?
+
+ directory = params[:path] ? "#{params[:path]}/" : ''
+ @entry = build.artifacts_metadata_entry(directory)
+
+ return render_404 unless @entry.exists?
+ end
+
+ def file
+ entry = build.artifacts_metadata_entry(params[:path])
+
+ if entry.exists?
+ render json: { archive: build.artifacts_file.path,
+ entry: Base64.encode64(entry.path) }
+ else
+ render json: {}, status: 404
+ end
+ end
+
+ private
+
+ def build
+ @build ||= project.builds.unscoped.find_by!(id: params[:build_id])
+ end
+
+ def artifacts_file
+ @artifacts_file ||= build.artifacts_file
+ end
+end
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 548f1b9ebfe..a6bebc46b06 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -1,16 +1,19 @@
class Projects::AvatarsController < Projects::ApplicationController
+ include BlobHelper
+
before_action :project
def show
- @blob = @project.repository.blob_at_branch('master', @project.avatar_in_git)
+ @blob = @repository.blob_at_branch('master', @project.avatar_in_git)
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
- send_data(
- @blob.data,
- type: @blob.mime_type,
- disposition: 'inline',
- filename: @blob.name
- )
+
+ return if cached_blob?
+
+ headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
+ headers['Content-Disposition'] = 'inline'
+ headers['Content-Type'] = safe_content_type(@blob)
+ head :ok # 'render nothing: true' messes up the Content-Type
else
render_404
end
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
new file mode 100644
index 00000000000..6ff47c4033a
--- /dev/null
+++ b/app/controllers/projects/badges_controller.rb
@@ -0,0 +1,13 @@
+class Projects::BadgesController < Projects::ApplicationController
+ before_action :no_cache_headers
+
+ def build
+ respond_to do |format|
+ format.html { render_404 }
+ format.svg do
+ image = Ci::ImageForBuildService.new.execute(project, ref: params[:ref])
+ send_file(image.path, filename: image.name, disposition: 'inline', type: 'image/svg+xml')
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 9ea518e6c85..f576d0be1fc 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -8,28 +8,6 @@ class Projects::BlameController < Projects::ApplicationController
def show
@blob = @repository.blob_at(@commit.id, @path)
- @blame = group_blame_lines
- end
-
- def group_blame_lines
- blame = Gitlab::Git::Blame.new(@repository, @commit.id, @path)
-
- prev_sha = nil
- groups = []
- current_group = nil
-
- blame.each do |commit, line|
- if prev_sha && prev_sha == commit.sha
- current_group[:lines] << line
- else
- groups << current_group if current_group.present?
- current_group = { commit: commit, lines: [line] }
- end
-
- prev_sha = commit.sha
- end
-
- groups << current_group if current_group.present?
- groups
+ @blame_groups = Gitlab::Blame.new(@blob, @commit).groups
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index c56a3497bb2..cd8b2911674 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -33,6 +33,7 @@ class Projects::BlobController < Projects::ApplicationController
def edit
@last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha
+ blob.load_all_data!(@repository)
end
def update
@@ -51,8 +52,11 @@ class Projects::BlobController < Projects::ApplicationController
def preview
@content = params[:content]
+ @blob.load_all_data!(@repository)
diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true)
- @diff_lines = Gitlab::Diff::Parser.new.parse(diffy.diff.scan(/.*\n/))
+ diff_lines = diffy.diff.scan(/.*\n/)[2..-1]
+ diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines)
+ @diff_lines = Gitlab::Diff::Highlight.new(diff_lines).highlight
render layout: false
end
@@ -65,8 +69,9 @@ class Projects::BlobController < Projects::ApplicationController
end
def diff
- @form = UnfoldForm.new(params)
- @lines = @blob.data.lines[@form.since - 1..@form.to - 1]
+ @form = UnfoldForm.new(params)
+ @lines = Gitlab::Highlight.highlight_lines(repository, @ref, @path)
+ @lines = @lines[@form.since - 1..@form.to - 1]
if @form.bottom?
@match_line = ''
@@ -82,7 +87,7 @@ class Projects::BlobController < Projects::ApplicationController
private
def blob
- @blob ||= @repository.blob_at(@commit.id, @path)
+ @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
if @blob
@blob
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 3c2849a7601..43ea717cbd2 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -9,6 +9,11 @@ class Projects::BranchesController < Projects::ApplicationController
@sort = params[:sort] || 'name'
@branches = @repository.branches_sorted_by(@sort)
@branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE)
+
+ @max_commits = @branches.reduce(0) do |memo, branch|
+ diverging_commit_counts = repository.diverging_commit_counts(branch)
+ [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+ end
end
def recent
@@ -18,11 +23,15 @@ class Projects::BranchesController < Projects::ApplicationController
def create
branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name)
- ref = sanitize(strip_tags(params[:ref]))
- ref = Addressable::URI.unescape(ref)
+
result = CreateBranchService.new(project, current_user).
execute(branch_name, ref)
+ if params[:issue_iid]
+ issue = @project.issues.find_by(iid: params[:issue_iid])
+ SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
+ end
+
if result[:status] == :success
@branch = result[:branch]
redirect_to namespace_project_tree_path(@project.namespace, @project,
@@ -44,4 +53,15 @@ class Projects::BranchesController < Projects::ApplicationController
format.js { render status: status[:return_code] }
end
end
+
+ private
+
+ def ref
+ if params[:ref]
+ ref_escaped = sanitize(strip_tags(params[:ref]))
+ Addressable::URI.unescape(ref_escaped)
+ else
+ @project.default_branch
+ end
+ end
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 26ba12520c7..f159e169f6d 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -1,10 +1,8 @@
class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
-
- before_action :authorize_manage_builds!, except: [:index, :show, :status]
- before_action :authorize_download_build_artifacts!, only: [:download]
-
- layout "project"
+ before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry]
+ before_action :authorize_update_build!, except: [:index, :show, :status]
+ layout 'project'
def index
@scope = params[:scope]
@@ -12,19 +10,18 @@ class Projects::BuildsController < Projects::ApplicationController
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
- when 'all'
- @builds
+ when 'running'
+ @builds.running_or_pending.reverse_order
when 'finished'
@builds.finished
else
- @builds.running_or_pending.reverse_order
+ @builds
end
@builds = @builds.page(params[:page]).per(30)
end
def cancel_all
@project.builds.running_or_pending.each(&:cancel)
-
redirect_to namespace_project_builds_path(project.namespace, project)
end
@@ -43,34 +40,26 @@ class Projects::BuildsController < Projects::ApplicationController
def retry
unless @build.retryable?
- return page_404
+ return render_404
end
build = Ci::Build.retry(@build)
-
redirect_to build_path(build)
end
- def download
- unless artifacts_file.file_storage?
- return redirect_to artifacts_file.url
- end
-
- unless artifacts_file.exists?
- return not_found!
- end
-
- send_file artifacts_file.path, disposition: 'attachment'
+ def cancel
+ @build.cancel
+ redirect_to build_path(@build)
end
def status
render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
end
- def cancel
- @build.cancel
-
- redirect_to build_path(@build)
+ def erase
+ @build.erase(erased_by: current_user)
+ redirect_to namespace_project_build_path(project.namespace, project, @build),
+ notice: "Build has been sucessfully erased!"
end
private
@@ -79,27 +68,7 @@ class Projects::BuildsController < Projects::ApplicationController
@build ||= project.builds.unscoped.find_by!(id: params[:id])
end
- def artifacts_file
- build.artifacts_file
- end
-
def build_path(build)
namespace_project_build_path(build.project.namespace, build.project, build)
end
-
- def authorize_manage_builds!
- unless can?(current_user, :manage_builds, project)
- return page_404
- end
- end
-
- def authorize_download_build_artifacts!
- unless can?(current_user, :download_build_artifacts, @project)
- if current_user.nil?
- return authenticate_user!
- else
- return render_404
- end
- end
- end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 0aaba3792bf..576fa3cedb2 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -2,16 +2,20 @@
#
# Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController
+ include CreatesCommit
+ include DiffHelper
+
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!, except: [:cancel_builds]
- before_action :authorize_manage_builds!, only: [:cancel_builds]
+ before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds]
+ before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
+ before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
- before_action :authorize_manage_builds!, only: [:cancel_builds, :retry_builds]
before_action :define_show_vars, only: [:show, :builds]
+ before_action :authorize_edit_tree!, only: [:revert]
def show
- return git_not_found! unless @commit
+ apply_diff_view_cookie!
@line_notes = commit.notes.inline
@note = @project.build_commit_note(commit)
@@ -55,8 +59,37 @@ class Projects::CommitController < Projects::ApplicationController
render layout: false
end
+ def revert
+ assign_revert_commit_vars
+
+ return render_404 if @target_branch.blank?
+
+ create_commit(Commits::RevertService, success_notice: "The #{revert_type_title} has been successfully reverted.",
+ success_path: successful_revert_path, failure_path: failed_revert_path)
+ end
+
private
+ def revert_type_title
+ @commit.merged_merge_request ? 'merge request' : 'commit'
+ end
+
+ def successful_revert_path
+ return referenced_merge_request_url if @commit.merged_merge_request
+
+ namespace_project_commits_url(@project.namespace, @project, @target_branch)
+ end
+
+ def failed_revert_path
+ return referenced_merge_request_url if @commit.merged_merge_request
+
+ namespace_project_commit_url(@project.namespace, @project, params[:id])
+ end
+
+ def referenced_merge_request_url
+ namespace_project_merge_request_url(@project.namespace, @project, @commit.merged_merge_request)
+ end
+
def commit
@commit ||= @project.commit(params[:id])
end
@@ -66,20 +99,27 @@ class Projects::CommitController < Projects::ApplicationController
end
def define_show_vars
- if params[:w].to_i == 1
- @diffs = commit.diffs({ ignore_whitespace_change: true })
- else
- @diffs = commit.diffs
- end
+ return git_not_found! unless commit
+
+ opts = diff_options
+ opts[:ignore_whitespace_change] = true if params[:format] == 'diff'
+ @diffs = commit.diffs(opts)
+ @diff_refs = [commit.parent || commit, commit]
@notes_count = commit.notes.count
@statuses = ci_commit.statuses if ci_commit
end
- def authorize_manage_builds!
- unless can?(current_user, :manage_builds, project)
- return page_404
- end
+ def assign_revert_commit_vars
+ @commit = project.commit(params[:id])
+ @target_branch = params[:target_branch]
+ @mr_source_branch = @commit.revert_branch_name
+ @mr_target_branch = @target_branch
+ @commit_params = {
+ commit: @commit,
+ revert_type_title: revert_type_title,
+ create_merge_request: params[:create_merge_request].present? || different_project?
+ }
end
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 04a88990bf4..1420b96840c 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -8,13 +8,22 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :authorize_download_code!
def show
- @repo = @project.repository
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
+ search = params[:search]
+
+ @commits =
+ if search.present?
+ @repository.find_commits_by_message(search, @ref, @path, @limit, @offset).compact
+ else
+ @repository.commits(@ref, @path, @limit, @offset)
+ end
- @commits = @repo.commits(@ref, @path, @limit, @offset)
@note_counts = project.notes.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
+ @merge_request = @project.merge_requests.opened.
+ find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
+
respond_to do |format|
format.html
format.json { pager_json("projects/commits/_commits", @commits.size) }
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 5200d609cc9..671d5c23024 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -1,27 +1,27 @@
require 'addressable/uri'
class Projects::CompareController < Projects::ApplicationController
+ include DiffHelper
+
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
+ before_action :assign_ref_vars, only: [:index, :show]
+ before_action :merge_request, only: [:index, :show]
def index
- @ref = Addressable::URI.unescape(params[:to])
end
def show
- base_ref = Addressable::URI.unescape(params[:from])
- @ref = head_ref = Addressable::URI.unescape(params[:to])
- diff_options = { ignore_whitespace_change: true } if params[:w] == '1'
-
- compare_result = CompareService.new.
- execute(@project, head_ref, @project, base_ref, diff_options)
-
- if compare_result
- @commits = Commit.decorate(compare_result.commits, @project)
- @diffs = compare_result.diffs
- @commit = @project.commit(head_ref)
- @first_commit = @project.commit(base_ref)
+ compare = CompareService.new.
+ execute(@project, @head_ref, @project, @base_ref, diff_options)
+
+ if compare
+ @commits = Commit.decorate(compare.commits, @project)
+ @commit = @project.commit(@head_ref)
+ @base_commit = @project.merge_base_commit(@base_ref, @head_ref)
+ @diffs = compare.diffs(diff_options)
+ @diff_refs = [@base_commit, @commit]
@line_notes = []
end
end
@@ -30,4 +30,16 @@ class Projects::CompareController < Projects::ApplicationController
redirect_to namespace_project_compare_path(@project.namespace, @project,
params[:from], params[:to])
end
+
+ private
+
+ def assign_ref_vars
+ @base_ref = Addressable::URI.unescape(params[:from])
+ @ref = @head_ref = Addressable::URI.unescape(params[:to])
+ end
+
+ def merge_request
+ @merge_request ||= @project.merge_requests.opened.
+ find_by(source_project: @project, source_branch: @head_ref, target_branch: @base_ref)
+ end
end
diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb
new file mode 100644
index 00000000000..54a0c447aee
--- /dev/null
+++ b/app/controllers/projects/find_file_controller.rb
@@ -0,0 +1,26 @@
+# Controller for viewing a repository's file structure
+class Projects::FindFileController < Projects::ApplicationController
+ include ExtractsPath
+ include ActionView::Helpers::SanitizeHelper
+ include TreeHelper
+
+ before_action :require_non_empty_project
+ before_action :assign_ref_vars
+ before_action :authorize_download_code!
+
+ def show
+ return render_404 unless @repository.commit(@ref)
+
+ respond_to do |format|
+ format.html
+ end
+ end
+
+ def list
+ file_paths = @repo.ls_files(@ref)
+
+ respond_to do |format|
+ format.json { render json: file_paths }
+ end
+ end
+end
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 750181f0c19..a1b8632df98 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -1,8 +1,33 @@
class Projects::ForksController < Projects::ApplicationController
+ include ContinueParams
+
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
+ def index
+ base_query = project.forks.includes(:creator)
+
+ @forks = base_query.merge(ProjectsFinder.new.execute(current_user))
+ @total_forks_count = base_query.size
+ @private_forks_count = @total_forks_count - @forks.size
+ @public_forks_count = @total_forks_count - @private_forks_count
+
+ @sort = params[:sort] || 'id_desc'
+ @forks = @forks.search(params[:filter_projects]) if params[:filter_projects].present?
+ @forks = @forks.order_by(@sort).page(params[:page]).per(PER_PAGE)
+
+ respond_to do |format|
+ format.html
+
+ format.json do
+ render json: {
+ html: view_to_html_string("projects/forks/_projects", projects: @forks)
+ }
+ end
+ end
+ end
+
def new
@namespaces = current_user.manageable_namespaces
@namespaces.delete(@project.namespace)
@@ -10,7 +35,7 @@ class Projects::ForksController < Projects::ApplicationController
def create
namespace = Namespace.find(params[:namespace_key])
-
+
@forked_project = namespace.projects.find_by(path: project.path)
@forked_project = nil unless @forked_project && @forked_project.forked_from_project == project
@@ -23,22 +48,11 @@ class Projects::ForksController < Projects::ApplicationController
if continue_params
redirect_to continue_params[:to], notice: continue_params[:notice]
else
- redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project was successfully forked."
+ redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project '#{@forked_project.name}' was successfully forked."
end
end
else
render :error
end
end
-
- private
-
- def continue_params
- continue_params = params[:continue]
- if continue_params
- continue_params.permit(:to, :notice, :notice_now)
- else
- nil
- end
- end
end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
new file mode 100644
index 00000000000..4159e53bfa9
--- /dev/null
+++ b/app/controllers/projects/group_links_controller.rb
@@ -0,0 +1,23 @@
+class Projects::GroupLinksController < Projects::ApplicationController
+ layout 'project_settings'
+ before_action :authorize_admin_project!
+
+ def index
+ @group_links = project.project_group_links.all
+ end
+
+ def create
+ link = project.project_group_links.new
+ link.group_id = params[:link_group_id]
+ link.group_access = params[:link_group_access]
+ link.save
+
+ redirect_to namespace_project_group_links_path(project.namespace, project)
+ end
+
+ def destroy
+ project.project_group_links.find(params[:id]).destroy
+
+ redirect_to namespace_project_group_links_path(project.namespace, project)
+ end
+end
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 8d8035ef5ff..7756f0f0ed3 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -1,8 +1,11 @@
class Projects::ImportsController < Projects::ApplicationController
+ include ContinueParams
+
# Authorize
before_action :authorize_admin_project!
- before_action :require_no_repo, except: :show
- before_action :redirect_if_progress, except: :show
+ before_action :require_no_repo, only: [:new, :create]
+ before_action :redirect_if_progress, only: [:new, :create]
+ before_action :redirect_if_no_import, only: :show
def new
end
@@ -24,11 +27,11 @@ class Projects::ImportsController < Projects::ApplicationController
end
def show
- if @project.repository_exists? || @project.import_finished?
+ if @project.import_finished?
if continue_params
redirect_to continue_params[:to], notice: continue_params[:notice]
else
- redirect_to project_path(@project), notice: "The project was successfully forked."
+ redirect_to namespace_project_path(@project.namespace, @project), notice: finished_notice
end
elsif @project.import_failed?
redirect_to new_namespace_project_import_path(@project.namespace, @project)
@@ -36,31 +39,36 @@ class Projects::ImportsController < Projects::ApplicationController
if continue_params && continue_params[:notice_now]
flash.now[:notice] = continue_params[:notice_now]
end
+
# Render
end
end
private
- def continue_params
- continue_params = params[:continue]
- if continue_params
- continue_params.permit(:to, :notice, :notice_now)
+ def finished_notice
+ if @project.forked?
+ 'The project was successfully forked.'
else
- nil
+ 'The project was successfully imported.'
end
end
def require_no_repo
- if @project.repository_exists? && !@project.import_in_progress?
- redirect_to(namespace_project_path(@project.namespace, @project))
+ if @project.repository_exists?
+ redirect_to namespace_project_path(@project.namespace, @project)
end
end
def redirect_if_progress
if @project.import_in_progress?
- redirect_to namespace_project_import_path(@project.namespace, @project) &&
- return
+ redirect_to namespace_project_import_path(@project.namespace, @project)
+ end
+ end
+
+ def redirect_if_no_import
+ if @project.repository_exists? && @project.no_import?
+ redirect_to namespace_project_path(@project.namespace, @project)
end
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b59b52291fb..6603f28a082 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -1,9 +1,11 @@
class Projects::IssuesController < Projects::ApplicationController
+ include ToggleSubscriptionAction
+
before_action :module_enabled
- before_action :issue, only: [:edit, :update, :show, :toggle_subscription]
+ before_action :issue, only: [:edit, :update, :show]
# Allow read any issue
- before_action :authorize_read_issue!
+ before_action :authorize_read_issue!, only: [:show]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
@@ -32,6 +34,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
@issues = @issues.page(params[:page]).per(PER_PAGE)
+ @label = @project.labels.find_by(title: params[:label_name])
respond_to do |format|
format.html
@@ -49,7 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
assignee_id: ""
)
- @issue = @project.issues.new(issue_params)
+ @issue = @noteable = @project.issues.new(issue_params)
respond_with(@issue)
end
@@ -61,7 +64,8 @@ class Projects::IssuesController < Projects::ApplicationController
@note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
- @merge_requests = @issue.referenced_merge_requests
+ @merge_requests = @issue.referenced_merge_requests(current_user)
+ @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch)
respond_with(@issue)
end
@@ -109,12 +113,6 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
end
- def toggle_subscription
- @issue.toggle_subscription(current_user)
-
- render nothing: true
- end
-
def closed_by_merge_requests
@closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
end
@@ -128,6 +126,11 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_old
end
end
+ alias_method :subscribable_resource, :issue
+
+ def authorize_read_issue!
+ return render_404 unless can?(current_user, :read_issue, @issue)
+ end
def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue)
@@ -159,7 +162,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
- :title, :assignee_id, :position, :description,
+ :title, :assignee_id, :position, :description, :confidential,
:milestone_id, :state_event, :task_num, label_ids: []
)
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 86d6e3e0f6b..40d8098690a 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -1,8 +1,12 @@
class Projects::LabelsController < Projects::ApplicationController
+ include ToggleSubscriptionAction
+
before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_read_label!
- before_action :authorize_admin_labels!, except: [:index]
+ before_action :authorize_admin_labels!, only: [
+ :new, :create, :edit, :update, :generate, :destroy
+ ]
respond_to :js, :html
@@ -69,12 +73,13 @@ class Projects::LabelsController < Projects::ApplicationController
end
def label_params
- params.require(:label).permit(:title, :color)
+ params.require(:label).permit(:title, :description, :color)
end
def label
- @label = @project.labels.find(params[:id])
+ @label ||= @project.labels.find(params[:id])
end
+ alias_method :subscribable_resource, :label
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index ab5c953189c..61b82c9db46 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -1,8 +1,11 @@
class Projects::MergeRequestsController < Projects::ApplicationController
+ include ToggleSubscriptionAction
+ include DiffHelper
+
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
- :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds
+ :ci_status, :cancel_merge_when_build_succeeds
]
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
@@ -34,6 +37,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
@merge_requests = @merge_requests.preload(:target_project)
+ @label = @project.labels.find_by(title: params[:label_name])
+
respond_to do |format|
format.html
format.json do
@@ -57,8 +62,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def diffs
+ apply_diff_view_cookie!
+
@commit = @merge_request.last_commit
- @first_commit = @merge_request.first_commit
+ @base_commit = @merge_request.diff_base_commit
+
+ # MRs created before 8.4 don't have a diff_base_commit,
+ # but we need it for the "View file @ ..." link by deleted files
+ @base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit
@comments_allowed = @reply_allowed = true
@comments_target = {
@@ -90,6 +101,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def new
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
+ @noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
@@ -101,8 +113,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.last_commit
- @first_commit = @merge_request.first_commit
- @diffs = @merge_request.compare_diffs
+ @base_commit = @merge_request.diff_base_commit
+ @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare
@ci_commit = @merge_request.ci_commit
@statuses = @ci_commit.statuses if @ci_commit
@@ -153,7 +165,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_check
- @merge_request.check_if_can_be_merged if @merge_request.unchecked?
+ @merge_request.check_if_can_be_merged
render partial: "projects/merge_requests/widget/show.html.haml", layout: false
end
@@ -172,6 +184,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return
end
+ TodoService.new.merge_merge_request(merge_request, current_user)
+
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active?
@@ -220,12 +234,6 @@ 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
@@ -239,6 +247,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_request
@merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end
+ alias_method :subscribable_resource, :merge_request
def closes_issues
@closes_issues ||= @merge_request.closes_issues
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 15506bd677a..da46731d945 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -11,11 +11,12 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to :html
def index
- @milestones = case params[:state]
- when 'all'; @project.milestones.order("state, due_date DESC")
- when 'closed'; @project.milestones.closed.order("due_date DESC")
- else @project.milestones.active.order("due_date ASC")
- end
+ @milestones =
+ case params[:state]
+ when 'all' then @project.milestones.reorder(due_date: :desc, title: :asc)
+ when 'closed' then @project.milestones.closed.reorder(due_date: :desc, title: :asc)
+ else @project.milestones.active.reorder(due_date: :asc, title: :asc)
+ end
@milestones = @milestones.includes(:project)
@milestones = @milestones.page(params[:page]).per(PER_PAGE)
@@ -31,9 +32,6 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def show
- @issues = @milestone.issues
- @users = @milestone.participants.uniq
- @merge_requests = @milestone.merge_requests
end
def create
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 6f1e186d408..1b9dd568043 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -11,11 +11,9 @@ class Projects::NotesController < Projects::ApplicationController
notes_json = { notes: [], last_fetched_at: current_fetched_at }
@notes.each do |note|
- notes_json[:notes] << {
- id: note.id,
- html: note_to_html(note),
- valid: note.valid?
- }
+ next if note.cross_reference_not_visible_for?(current_user)
+
+ notes_json[:notes] << note_json(note)
end
render json: notes_json
@@ -25,7 +23,7 @@ class Projects::NotesController < Projects::ApplicationController
@note = Notes::CreateService.new(project, current_user, note_params).execute
respond_to do |format|
- format.json { render_note_json(@note) }
+ format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
@@ -34,7 +32,7 @@ class Projects::NotesController < Projects::ApplicationController
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
respond_to do |format|
- format.json { render_note_json(@note) }
+ format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
@@ -99,6 +97,8 @@ class Projects::NotesController < Projects::ApplicationController
end
def note_to_discussion_html(note)
+ return unless note.for_diff_line?
+
if params[:view] == 'parallel'
template = "projects/notes/_diff_notes_with_reply_parallel"
locals =
@@ -106,7 +106,7 @@ class Projects::NotesController < Projects::ApplicationController
{ notes_left: [note], notes_right: [] }
else
{ notes_left: [], notes_right: [note] }
- end
+ end
else
template = "projects/notes/_diff_notes_with_reply"
locals = { notes: [note] }
@@ -131,9 +131,9 @@ class Projects::NotesController < Projects::ApplicationController
)
end
- def render_note_json(note)
+ def note_json(note)
if note.valid?
- render json: {
+ {
valid: true,
id: note.id,
discussion_id: note.discussion_id,
@@ -144,7 +144,7 @@ class Projects::NotesController < Projects::ApplicationController
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
}
else
- render json: {
+ {
valid: false,
award: note.is_award,
errors: note.errors
@@ -163,8 +163,6 @@ class Projects::NotesController < Projects::ApplicationController
)
end
- private
-
def find_current_user_notes
@notes = NotesFinder.new.execute(project, current_user, params)
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 8364fc293b7..e7bddc4a6f1 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -27,6 +27,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
@project_member = @project.project_members.new
+ @project_group_links = @project.project_group_links
end
def create
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index be7d5c187fe..10de0e60530 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -1,6 +1,7 @@
# Controller for viewing a file's raw
class Projects::RawController < Projects::ApplicationController
include ExtractsPath
+ include BlobHelper
before_action :require_non_empty_project
before_action :assign_ref_vars
@@ -12,10 +13,15 @@ class Projects::RawController < Projects::ApplicationController
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
+ return if cached_blob?
+
if @blob.lfs_pointer?
send_lfs_object
else
- stream_data
+ headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
+ headers['Content-Disposition'] = 'inline'
+ headers['Content-Type'] = safe_content_type(@blob)
+ head :ok # 'render nothing: true' messes up the Content-Type
end
else
render_404
@@ -24,26 +30,6 @@ class Projects::RawController < Projects::ApplicationController
private
- def get_blob_type
- if @blob.text?
- 'text/plain; charset=utf-8'
- elsif @blob.image?
- @blob.content_type
- else
- 'application/octet-stream'
- end
- end
-
- def stream_data
- type = get_blob_type
-
- send_data(
- @blob.data,
- type: type,
- disposition: 'inline'
- )
- end
-
def send_lfs_object
lfs_object = find_lfs_object
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index c4e18c17077..00df1c9c965 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -20,6 +20,8 @@ class Projects::RefsController < Projects::ApplicationController
namespace_project_network_path(@project.namespace, @project, @id, @options)
when "graphs"
namespace_project_graph_path(@project.namespace, @project, @id)
+ when "find_file"
+ namespace_project_find_file_path(@project.namespace, @project, @id)
when "graphs_commits"
commits_namespace_project_graph_path(@project.namespace, @project, @id)
else
@@ -62,9 +64,9 @@ class Projects::RefsController < Projects::ApplicationController
}
end
- if @logs.present?
- @log_url = namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/'))
- @more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit))
+ offset = (@offset + @limit)
+ if contents.size > offset
+ @more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: offset)
end
respond_to do |format|
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index ba9aea1c165..5c7614cfbaf 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -11,7 +11,9 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def archive
- render json: ArchiveRepositoryService.new(@project, params[:ref], params[:format]).execute
+ RepositoryArchiveCacheWorker.perform_async
+ headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format]))
+ head :ok
rescue => ex
logger.error("#{self.class.name}: #{ex}")
return git_not_found!
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index e2785caa2fb..bedeb4a295c 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -1,5 +1,5 @@
class Projects::RunnerProjectsController < Projects::ApplicationController
- before_action :authorize_admin_project!
+ before_action :authorize_admin_build!
layout 'project_settings'
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index 4993b2648a5..0dd2d6a99be 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -1,6 +1,6 @@
class Projects::RunnersController < Projects::ApplicationController
+ before_action :authorize_admin_build!
before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
- before_action :authorize_admin_project!
layout 'project_settings'
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 2104c7a7a71..92b0caa2efb 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -25,7 +25,7 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def new
- @snippet = @project.snippets.build
+ @snippet = @noteable = @project.snippets.build
end
def create
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 280fe12cc7c..e580487a2c6 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -34,6 +34,11 @@ class Projects::TagsController < Projects::ApplicationController
def destroy
DeleteTagService.new(project, current_user).execute(params[:id])
- redirect_to namespace_project_tags_path(@project.namespace, @project)
+ respond_to do |format|
+ format.html do
+ redirect_to namespace_project_tags_path(@project.namespace, @project)
+ end
+ format.js
+ end
end
end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index 30adfad1daa..92359745cec 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -1,5 +1,5 @@
class Projects::TriggersController < Projects::ApplicationController
- before_action :authorize_admin_project!
+ before_action :authorize_admin_build!
layout 'project_settings'
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 10efafea9db..00234654578 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -1,5 +1,5 @@
class Projects::VariablesController < Projects::ApplicationController
- before_action :authorize_admin_project!
+ before_action :authorize_admin_build!
layout 'project_settings'
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 3004722bce0..c9930480770 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,14 +1,13 @@
class ProjectsController < ApplicationController
include ExtractsPath
- prepend_before_action :render_go_import, only: [:show]
skip_before_action :authenticate_user!, only: [:show, :activity]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
# Authorize
- before_action :authorize_admin_project!, only: [:edit, :update]
+ before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping]
before_action :event_filter, only: [:show, :activity]
layout :determine_layout
@@ -93,6 +92,10 @@ class ProjectsController < ApplicationController
return
end
+ if @project.pending_delete?
+ flash[:alert] = "Project queued for delete."
+ end
+
respond_to do |format|
format.html do
if @project.repository_exists?
@@ -120,8 +123,8 @@ class ProjectsController < ApplicationController
def destroy
return access_denied! unless can?(current_user, :remove_project, @project)
- ::Projects::DestroyService.new(@project, current_user, {}).execute
- flash[:alert] = "Project '#{@project.name}' was deleted."
+ ::Projects::DestroyService.new(@project, current_user, {}).pending_delete!
+ flash[:alert] = "Project '#{@project.name}' will be deleted."
redirect_to dashboard_projects_path
rescue Projects::DestroyService::DestroyError => ex
@@ -131,7 +134,7 @@ class ProjectsController < ApplicationController
def autocomplete_sources
note_type = params['type']
note_id = params['type_id']
- autocomplete = ::Projects::AutocompleteService.new(@project)
+ autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = {
@@ -166,6 +169,20 @@ class ProjectsController < ApplicationController
end
end
+ def housekeeping
+ ::Projects::HousekeepingService.new(@project).execute
+
+ redirect_to(
+ project_path(@project),
+ notice: "Housekeeping successfully started"
+ )
+ rescue ::Projects::HousekeepingService::LeaseTaken => ex
+ redirect_to(
+ edit_project_path(@project),
+ alert: ex.to_s
+ )
+ end
+
def toggle_star
current_user.toggle_star(@project)
@project.reload
@@ -214,6 +231,7 @@ class ProjectsController < ApplicationController
:issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
+ :public_builds,
)
end
@@ -222,22 +240,12 @@ class ProjectsController < ApplicationController
Emoji.emojis.map do |name, emoji|
{
name: name,
- path: view_context.image_url("emoji/#{emoji["unicode"]}.png")
+ path: view_context.image_url("#{emoji["unicode"]}.png")
}
end
end
end
- def render_go_import
- return unless params["go-get"] == "1"
-
- @namespace = params[:namespace_id]
- @id = params[:project_id] || params[:id]
- @id = @id.gsub(/\.git\Z/, "")
-
- render "go_import", layout: false
- end
-
def repo_exists?
project.repository_exists? && !project.empty_repo?
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 9bb42ec86b3..e42d2d73947 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -1,4 +1,6 @@
class SearchController < ApplicationController
+ skip_before_action :authenticate_user!, :reject_blocked
+
include SearchHelper
layout 'search'
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
new file mode 100644
index 00000000000..7271c933b9b
--- /dev/null
+++ b/app/controllers/sent_notifications_controller.rb
@@ -0,0 +1,25 @@
+class SentNotificationsController < ApplicationController
+ skip_before_action :authenticate_user!
+
+ def unsubscribe
+ @sent_notification = SentNotification.for(params[:id])
+ return render_404 unless @sent_notification && @sent_notification.unsubscribable?
+
+ noteable = @sent_notification.noteable
+ noteable.unsubscribe(@sent_notification.recipient)
+
+ flash[:notice] = "You have been unsubscribed from this thread."
+ if current_user
+ case noteable
+ when Issue
+ redirect_to issue_path(noteable)
+ when MergeRequest
+ redirect_to merge_request_path(noteable)
+ else
+ redirect_to root_path
+ end
+ else
+ redirect_to new_user_session_path
+ end
+ end
+end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 825f85199be..65677a3dd3c 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -2,8 +2,12 @@ class SessionsController < Devise::SessionsController
include AuthenticatesWithTwoFactor
include Recaptcha::ClientHelper
+ skip_before_action :check_2fa_requirement, only: [:destroy]
+
+ prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor, only: [:create]
prepend_before_action :store_redirect_path, only: [:new]
+
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
@@ -31,6 +35,22 @@ class SessionsController < Devise::SessionsController
private
+ # Handle an "initial setup" state, where there's only one user, it's an admin,
+ # and they require a password change.
+ def check_initial_setup
+ return unless User.count == 1
+
+ user = User.admins.last
+
+ return unless user && user.require_password?
+
+ token = user.generate_reset_token
+ user.save
+
+ redirect_to edit_user_password_path(reset_password_token: token),
+ notice: "Please create a password for your new account."
+ end
+
def user_params
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt)
end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 868b05929d7..509f4f412ca 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -55,14 +55,15 @@ class UploadsController < ApplicationController
"user" => User,
"project" => Project,
"note" => Note,
- "group" => Group
+ "group" => Group,
+ "appearance" => Appearance
}
upload_models[params[:model]]
end
def upload_mount
- upload_mounts = %w(avatar attachment file)
+ upload_mounts = %w(avatar attachment file logo header_logo)
if upload_mounts.include?(params[:mounted_as])
params[:mounted_as]
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 30cb869eb2a..e10c633690f 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -3,12 +3,6 @@ class UsersController < ApplicationController
before_action :set_user
def show
- @contributed_projects = contributed_projects.joined(@user).reject(&:forked?)
-
- @projects = PersonalProjectsFinder.new(@user).execute(current_user)
-
- @groups = JoinedGroupsFinder.new(@user).execute(current_user)
-
respond_to do |format|
format.html
@@ -24,6 +18,45 @@ class UsersController < ApplicationController
end
end
+ def groups
+ load_groups
+
+ respond_to do |format|
+ format.html { render 'show' }
+ format.json do
+ render json: {
+ html: view_to_html_string("shared/groups/_list", groups: @groups)
+ }
+ end
+ end
+ end
+
+ def projects
+ load_projects
+
+ respond_to do |format|
+ format.html { render 'show' }
+ format.json do
+ render json: {
+ html: view_to_html_string("shared/projects/_list", projects: @projects, remote: true)
+ }
+ end
+ end
+ end
+
+ def contributed
+ load_contributed_projects
+
+ respond_to do |format|
+ format.html { render 'show' }
+ format.json do
+ render json: {
+ html: view_to_html_string("shared/projects/_list", projects: @contributed_projects)
+ }
+ end
+ end
+ end
+
def calendar
calendar = contributions_calendar
@timestamps = calendar.timestamps
@@ -34,12 +67,8 @@ class UsersController < ApplicationController
end
def calendar_activities
- @calendar_date = Date.parse(params[:date]) rescue nil
- @events = []
-
- if @calendar_date
- @events = contributions_calendar.events_by_date(@calendar_date)
- end
+ @calendar_date = Date.parse(params[:date]) rescue Date.today
+ @events = contributions_calendar.events_by_date(@calendar_date)
render 'calendar_activities', layout: false
end
@@ -56,7 +85,7 @@ class UsersController < ApplicationController
def contributions_calendar
@contributions_calendar ||= Gitlab::ContributionsCalendar.
- new(contributed_projects.reject(&:forked?), @user)
+ new(contributed_projects, @user)
end
def load_events
@@ -68,6 +97,20 @@ class UsersController < ApplicationController
limit_recent(20, params[:offset])
end
+ def load_projects
+ @projects =
+ PersonalProjectsFinder.new(@user).execute(current_user)
+ .page(params[:page]).per(PER_PAGE)
+ end
+
+ def load_contributed_projects
+ @contributed_projects = contributed_projects.joined(@user)
+ end
+
+ def load_groups
+ @groups = @user.groups.order_id_desc
+ end
+
def projects_for_current_user
ProjectsFinder.new.execute(current_user)
end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
deleted file mode 100644
index 91cb0f228f0..00000000000
--- a/app/finders/groups_finder.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-class GroupsFinder
- # Finds the groups available to the given user.
- #
- # current_user - The user to find the groups for.
- #
- # Returns an ActiveRecord::Relation.
- def execute(current_user = nil)
- if current_user
- relation = groups_visible_to_user(current_user)
- else
- relation = public_groups
- end
-
- relation.order_id_desc
- end
-
- private
-
- # This method returns the groups "current_user" can see.
- def groups_visible_to_user(current_user)
- base = groups_for_projects(public_and_internal_projects)
-
- union = Gitlab::SQL::Union.
- new([base.select(:id), current_user.authorized_groups.select(:id)])
-
- Group.where("namespaces.id IN (#{union.to_sql})")
- end
-
- def public_groups
- groups_for_projects(public_projects)
- end
-
- def groups_for_projects(projects)
- Group.public_and_given_groups(projects.select(:namespace_id))
- end
-
- def public_projects
- Project.unscoped.public_only
- end
-
- def public_and_internal_projects
- Project.unscoped.public_and_internal_only
- end
-end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 3d5e8b6fbe7..19e8c7a92be 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -79,9 +79,10 @@ class IssuableFinder
if project?
@projects = project
elsif current_user && params[:authorized_only].presence && !current_user_related?
- @projects = current_user.authorized_projects
+ @projects = current_user.authorized_projects.reorder(nil)
else
- @projects = ProjectsFinder.new.execute(current_user)
+ @projects = ProjectsFinder.new.execute(current_user, group: group).
+ reorder(nil)
end
end
@@ -118,6 +119,20 @@ class IssuableFinder
labels? && params[:label_name] == Label::None.title
end
+ def labels
+ return @labels if defined?(@labels)
+
+ if labels? && !filter_by_no_label?
+ @labels = Label.where(title: label_names)
+
+ if projects
+ @labels = @labels.where(project: projects)
+ end
+ else
+ @labels = Label.none
+ end
+ end
+
def assignee?
params[:assignee_id].present?
end
@@ -229,10 +244,17 @@ class IssuableFinder
items
end
+ def filter_by_upcoming_milestone?
+ params[:milestone_title] == '#upcoming'
+ end
+
def by_milestone(items)
if milestones?
if filter_by_no_milestone?
items = items.where(milestone_id: [-1, nil])
+ elsif filter_by_upcoming_milestone?
+ upcoming = Milestone.where(project_id: projects).upcoming
+ items = items.joins(:milestone).where(milestones: { title: upcoming.title })
else
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
@@ -248,13 +270,9 @@ class IssuableFinder
def by_label(items)
if labels?
if filter_by_no_label?
- items = items.
- joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id").
- where(label_links: { id: nil })
+ items = items.without_label
else
- label_names = params[:label_name].split(",")
-
- items = items.joins(:labels).where(labels: { title: label_names })
+ items = items.with_label(label_names)
if projects
items = items.where(labels: { project_id: projects })
@@ -265,6 +283,10 @@ class IssuableFinder
items
end
+ def label_names
+ params[:label_name].split(',')
+ end
+
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 20a2b0ce8f0..c2befa5a5b3 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -19,4 +19,10 @@ class IssuesFinder < IssuableFinder
def klass
Issue
end
+
+ private
+
+ def init_collection
+ Issue.visible_to_user(current_user)
+ end
end
diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb
deleted file mode 100644
index e7523136fea..00000000000
--- a/app/finders/joined_groups_finder.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# Class for finding the groups a user is a member of.
-class JoinedGroupsFinder
- def initialize(user = nil)
- @user = user
- end
-
- # Finds the groups of the source user, optionally limited to those visible to
- # the current user.
- #
- # current_user - If given the groups of "@user" will only include the groups
- # "current_user" can also see.
- #
- # Returns an ActiveRecord::Relation.
- def execute(current_user = nil)
- if current_user
- relation = groups_visible_to_user(current_user)
- else
- relation = public_groups
- end
-
- relation.order_id_desc
- end
-
- private
-
- # Returns the groups the user in "current_user" can see.
- #
- # This list includes all public/internal projects as well as the projects of
- # "@user" that "current_user" also has access to.
- def groups_visible_to_user(current_user)
- base = @user.authorized_groups.visible_to_user(current_user)
- extra = public_and_internal_groups
- union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)])
-
- Group.where("namespaces.id IN (#{union.to_sql})")
- end
-
- def public_groups
- groups_for_projects(@user.authorized_projects.public_only)
- end
-
- def public_and_internal_groups
- groups_for_projects(@user.authorized_projects.public_and_internal_only)
- end
-
- def groups_for_projects(projects)
- @user.groups.public_and_given_groups(projects.select(:namespace_id))
- end
-end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 3b4e0362e04..3a5fc5b5907 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -40,21 +40,26 @@ class ProjectsFinder
private
def group_projects(current_user, group)
- if current_user
- [
- group_projects_for_user(current_user, group),
- group.projects.public_and_internal_only
- ]
+ return [group.projects.public_only] unless current_user
+
+ user_group_projects = [
+ group_projects_for_user(current_user, group),
+ group.shared_projects.visible_to_user(current_user)
+ ]
+ if current_user.external?
+ user_group_projects << group.projects.public_only
else
- [group.projects.public_only]
+ user_group_projects << group.projects.public_and_internal_only
end
end
def all_projects(current_user)
- if current_user
- [current_user.authorized_projects, public_and_internal_projects]
+ return [public_projects] unless current_user
+
+ if current_user.external?
+ [current_user.authorized_projects, public_projects]
else
- [Project.public_only]
+ [current_user.authorized_projects, public_and_internal_projects]
end
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 07b5759443b..a41172816b8 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -4,7 +4,7 @@ class SnippetsFinder
case filter
when :all then
- snippets(current_user).fresh.non_expired
+ snippets(current_user).fresh
when :by_user then
by_user(current_user, params[:user], params[:scope])
when :by_project
@@ -27,7 +27,7 @@ class SnippetsFinder
end
def by_user(current_user, user, scope)
- snippets = user.snippets.fresh.non_expired
+ snippets = user.snippets.fresh
return snippets.are_public unless current_user
@@ -48,7 +48,7 @@ class SnippetsFinder
end
def by_project(current_user, project)
- snippets = project.snippets.fresh.non_expired
+ snippets = project.snippets.fresh
if current_user
if project.team.member?(current_user.id)
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
new file mode 100644
index 00000000000..3ba27c40504
--- /dev/null
+++ b/app/finders/todos_finder.rb
@@ -0,0 +1,129 @@
+# TodosFinder
+#
+# Used to filter Todos by set of params
+#
+# Arguments:
+# current_user - which user use
+# params:
+# action_id: integer
+# author_id: integer
+# project_id; integer
+# state: 'pending' or 'done'
+# type: 'Issue' or 'MergeRequest'
+#
+
+class TodosFinder
+ NONE = '0'
+
+ attr_accessor :current_user, :params
+
+ def initialize(current_user, params)
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ items = current_user.todos
+ items = by_action_id(items)
+ items = by_author(items)
+ items = by_project(items)
+ items = by_state(items)
+ items = by_type(items)
+
+ items
+ end
+
+ private
+
+ def action_id?
+ action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED].include?(action_id.to_i)
+ end
+
+ def action_id
+ params[:action_id]
+ end
+
+ def author?
+ params[:author_id].present?
+ end
+
+ def author
+ return @author if defined?(@author)
+
+ @author =
+ if author? && params[:author_id] != NONE
+ User.find(params[:author_id])
+ else
+ nil
+ end
+ end
+
+ def project?
+ params[:project_id].present?
+ end
+
+ def project
+ return @project if defined?(@project)
+
+ if project?
+ @project = Project.find(params[:project_id])
+
+ unless Ability.abilities.allowed?(current_user, :read_project, @project)
+ @project = nil
+ end
+ else
+ @project = nil
+ end
+
+ @project
+ end
+
+ def type?
+ type.present? && ['Issue', 'MergeRequest'].include?(type)
+ end
+
+ def type
+ params[:type]
+ end
+
+ def by_action_id(items)
+ if action_id?
+ items = items.where(action: action_id)
+ end
+
+ items
+ end
+
+ def by_author(items)
+ if author?
+ items = items.where(author_id: author.try(:id))
+ end
+
+ items
+ end
+
+ def by_project(items)
+ if project?
+ items = items.where(project: project)
+ end
+
+ items
+ end
+
+ def by_state(items)
+ case params[:state]
+ when 'done'
+ items.done
+ else
+ items.pending
+ end
+ end
+
+ def by_type(items)
+ if type?
+ items = items.where(target_type: type)
+ end
+
+ items
+ end
+end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index c5820bf4c50..e0abc3a2869 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -1,21 +1,33 @@
module AppearancesHelper
- def brand_item
- nil
- end
-
def brand_title
- 'GitLab Community Edition'
+ if brand_item && brand_item.title
+ brand_item.title
+ else
+ 'GitLab Community Edition'
+ end
end
def brand_image
- nil
+ if brand_item.logo?
+ image_tag brand_item.logo
+ else
+ nil
+ end
end
def brand_text
- nil
+ markdown(brand_item.description)
+ end
+
+ def brand_item
+ @appearance ||= Appearance.first
end
def brand_header_logo
- render 'shared/logo.svg'
+ if brand_item && brand_item.header_logo?
+ image_tag brand_item.header_logo
+ else
+ render 'shared/logo.svg'
+ end
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index f7f7a1a02d3..e6ceb213532 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User)
user = user_or_email
else
- user = User.find_by(email: user_or_email.downcase)
+ user = User.find_by_any_email(user_or_email.try(:downcase))
end
if user
@@ -118,12 +118,6 @@ module ApplicationHelper
grouped_options_for_select(options, @ref || @project.default_branch)
end
- def emoji_autocomplete_source
- # should be an array of strings
- # so to_s can be called, because it is sufficient and to_json is too slow
- Emoji.names.to_s
- end
-
# Define whenever show last push event
# with suggestion to create MR
def show_last_push_widget?(event)
@@ -169,22 +163,6 @@ module ApplicationHelper
Gitlab.config.extra
end
- def search_placeholder
- if @project && @project.persisted?
- 'Search in this project'
- elsif @snippet || @snippets || @show_snippets
- 'Search snippets'
- elsif @group && @group.persisted?
- 'Search in this group'
- else
- 'Search'
- end
- end
-
- def broadcast_message
- BroadcastMessage.current
- end
-
# Render a `time` element with Javascript-based relative date and tooltip
#
# time - Time object
@@ -204,9 +182,9 @@ module ApplicationHelper
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
element = content_tag :time, time.to_s,
- class: "#{html_class} js-timeago js-timeago-pending",
- datetime: time.getutc.iso8601,
- title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
+ class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}",
+ datetime: time.to_time.getutc.iso8601,
+ title: time.in_time_zone.to_s(:medium),
data: { toggle: 'tooltip', placement: placement, container: 'body' }
unless skip_js
@@ -218,6 +196,22 @@ module ApplicationHelper
element
end
+ def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', include_author: false)
+ return if object.updated_at == object.created_at
+
+ content_tag :small, class: "edited-text" do
+ output = content_tag(:span, "Edited ")
+ output << time_ago_with_tooltip(object.updated_at, placement: placement, html_class: html_class)
+
+ if include_author && object.updated_by && object.updated_by != object.author
+ output << content_tag(:span, " by ")
+ output << link_to_member(object.project, object.updated_by, avatar: false, author_class: nil)
+ end
+
+ output
+ end
+ end
+
def render_markup(file_name, file_content)
if gitlab_markdown?(file_name)
Haml::Helpers.preserve(markdown(file_content))
@@ -228,8 +222,7 @@ module ApplicationHelper
file_content
end
else
- GitHub::Markup.render(file_name, file_content).
- force_encoding(file_content.encoding).html_safe
+ other_markup(file_name, file_content)
end
rescue RuntimeError
simple_format(file_content)
@@ -266,7 +259,7 @@ module ApplicationHelper
state: params[:state],
scope: params[:scope],
label_name: params[:label_name],
- milestone_id: params[:milestone_id],
+ milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
author_id: params[:author_id],
sort: params[:sort],
@@ -308,7 +301,7 @@ module ApplicationHelper
if project.nil?
nil
elsif current_controller?(:issues)
- project.issues.send(entity).count
+ project.issues.visible_to_user(current_user).send(entity).count
elsif current_controller?(:merge_requests)
project.merge_requests.send(entity).count
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 7d6b58ee21a..23693629a4c 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -23,6 +23,10 @@ module ApplicationSettingsHelper
current_application_settings.user_oauth_applications
end
+ def askimet_enabled?
+ current_application_settings.akismet_enabled?
+ end
+
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
def restricted_level_checkboxes(help_block_id)
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 0cfc0565e84..b4f80fd9b3e 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,11 +1,15 @@
module AuthHelper
- PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook).freeze
+ PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
Gitlab.config.ldap.enabled
end
+ def omniauth_enabled?
+ Gitlab.config.omniauth.enabled
+ end
+
def provider_has_icon?(name)
PROVIDERS_WITH_ICONS.include?(name.to_s)
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index d31d4cde08f..0f77b3b299a 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -1,21 +1,10 @@
module BlobHelper
- def highlight(blob_name, blob_content, nowrap: false, continue: false)
- @formatter ||= Rouge::Formatters::HTMLGitlab.new(
- nowrap: nowrap,
- cssclass: 'code highlight',
- lineanchors: true,
- lineanchorsid: 'LC'
- )
-
- begin
- @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new
- result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe
- rescue
- @lexer = Rouge::Lexers::PlainText
- result = @formatter.format(@lexer.lex(blob_content)).html_safe
- end
+ def highlighter(blob_name, blob_content, nowrap: false)
+ Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap)
+ end
- result
+ def highlight(blob_name, blob_content, nowrap: false)
+ Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap)
end
def no_highlight_files
@@ -37,20 +26,19 @@ module BlobHelper
tree_join(ref, path),
link_opts)
- if !on_top_of_branch?
+ if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn btn-default disabled has_tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
- elsif can_edit_blob?(blob)
- link_to "Edit", edit_path, class: 'btn btn-small'
+ elsif can_edit_blob?(blob, project, ref)
+ link_to "Edit", edit_path, class: 'btn'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: edit_path,
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
- fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id,
- continue: continue_params)
+ fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
- link_to "Edit", fork_path, class: 'btn btn-small', method: :post
+ link_to "Edit", fork_path, class: 'btn', method: :post
end
end
@@ -61,11 +49,11 @@ module BlobHelper
return unless blob
- if !on_top_of_branch?
+ if !on_top_of_branch?(project, ref)
button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.lfs_pointer?
button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
- elsif can_edit_blob?(blob)
+ elsif can_edit_blob?(blob, project, ref)
button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
continue_params = {
@@ -73,8 +61,7 @@ module BlobHelper
notice: edit_in_new_fork_notice + " Try to #{action} this file again.",
notice_now: edit_in_new_fork_notice_now
}
- fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id,
- continue: continue_params)
+ fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post
end
@@ -139,4 +126,51 @@ module BlobHelper
blob.size
end
end
+
+ # SVGs can contain malicious JavaScript; only include whitelisted
+ # elements and attributes. Note that this whitelist is by no means complete
+ # and may omit some elements.
+ def sanitize_svg(blob)
+ blob.data = Loofah.scrub_fragment(blob.data, :strip).to_xml
+ blob
+ end
+
+ # If we blindly set the 'real' content type when serving a Git blob we
+ # are enabling XSS attacks. An attacker could upload e.g. a Javascript
+ # file to a Git repository, trick the browser of a victim into
+ # downloading the blob, and then the 'application/javascript' content
+ # type would tell the browser to execute the attacker's Javascript. By
+ # overriding the content type and setting it to 'text/plain' (in the
+ # example of Javascript) we tell the browser of the victim not to
+ # execute untrusted data.
+ def safe_content_type(blob)
+ if blob.text?
+ 'text/plain; charset=utf-8'
+ elsif blob.image?
+ blob.content_type
+ else
+ 'application/octet-stream'
+ end
+ end
+
+ def cached_blob?
+ stale = stale?(etag: @blob.id) # The #stale? method sets cache headers.
+
+ # Because we are opionated we set the cache headers ourselves.
+ response.cache_control[:public] = @project.public?
+
+ if @ref && @commit && @ref == @commit.id
+ # This is a link to a commit by its commit SHA. That means that the blob
+ # is immutable. The only reason to invalidate the cache is if the commit
+ # was deleted or if the user lost access to the repository.
+ response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE
+ else
+ # A branch or tag points at this blob. That means that the expected blob
+ # value may change over time.
+ response.cache_control[:max_age] = Blob::CACHE_TIME
+ end
+
+ response.etag = @blob.id
+ !stale
+ end
end
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
index 6484dca6b55..43a29c96bca 100644
--- a/app/helpers/broadcast_messages_helper.rb
+++ b/app/helpers/broadcast_messages_helper.rb
@@ -1,16 +1,38 @@
module BroadcastMessagesHelper
- def broadcast_styling(broadcast_message)
- styling = ''
+ def broadcast_message(message = BroadcastMessage.current)
+ return unless message.present?
+
+ content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
+ icon('bullhorn') << ' ' << render_broadcast_message(message.message)
+ end
+ end
+
+ def broadcast_message_style(broadcast_message)
+ style = ''
if broadcast_message.color.present?
- styling << "background-color: #{broadcast_message.color}"
- styling << '; ' if broadcast_message.font.present?
+ style << "background-color: #{broadcast_message.color}"
+ style << '; ' if broadcast_message.font.present?
end
if broadcast_message.font.present?
- styling << "color: #{broadcast_message.font}"
+ style << "color: #{broadcast_message.font}"
end
- styling
+ style
+ end
+
+ def broadcast_message_status(broadcast_message)
+ if broadcast_message.active?
+ 'Active'
+ elsif broadcast_message.ended?
+ 'Expired'
+ else
+ 'Pending'
+ end
+ end
+
+ def render_broadcast_message(message)
+ Banzai.render(message, pipeline: :broadcast_message).html_safe
end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index ec0e3f409c1..d6c05843743 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -17,7 +17,7 @@ module ButtonHelper
def clipboard_button(data = {})
content_tag :button,
icon('clipboard'),
- class: 'btn btn-xs btn-clipboard',
+ class: 'btn btn-clipboard',
data: data,
type: :button
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index d8bee21c82e..8b1575d5e0c 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -12,9 +12,13 @@ module CiStatusHelper
ci_label_for_status(ci_commit.status)
end
- def ci_status_with_icon(status)
- content_tag :span, class: "ci-status ci-#{status}" do
- ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
+ def ci_status_with_icon(status, target = nil)
+ content = ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
+ klass = "ci-status ci-#{status}"
+ if target
+ link_to content, target, class: klass
+ else
+ content_tag :span, content, class: klass
end
end
@@ -42,12 +46,12 @@ module CiStatusHelper
icon(icon_name + ' fw')
end
- def render_ci_status(ci_commit)
+ def render_ci_status(ci_commit, tooltip_placement: 'auto left')
link_to ci_status_icon(ci_commit),
ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
title: "Build #{ci_status_label(ci_commit)}",
- data: { toggle: 'tooltip', placement: 'left' }
+ data: { toggle: 'tooltip', placement: tooltip_placement }
end
def no_runners_for_project?(project)
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 590d20ac7b3..f994c9e6170 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -123,6 +123,37 @@ module CommitsHelper
)
end
+ def revert_commit_link(commit, continue_to_path, btn_class: nil)
+ return unless current_user
+
+ tooltip = "Revert this #{revert_commit_type(commit)} in a new merge request"
+
+ if can_collaborate_with_project?
+ content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do
+ link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}"
+ end
+ elsif can?(current_user, :fork_project, @project)
+ continue_params = {
+ to: continue_to_path,
+ notice: edit_in_new_fork_notice + ' Try to revert this commit again.',
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = namespace_project_forks_path(@project.namespace, @project,
+ namespace_key: current_user.namespace.id,
+ continue: continue_params)
+
+ link_to 'Revert', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip
+ end
+ end
+
+ def revert_commit_type(commit)
+ if commit.merged_merge_request
+ 'merge request'
+ else
+ 'commit'
+ end
+ end
+
protected
# Private: Returns a link to a person. If the person has a matching user and
@@ -152,7 +183,7 @@ module CommitsHelper
options = {
class: "commit-#{options[:source]}-link has_tooltip",
- data: { :'original-title' => sanitize(source_email) }
+ data: { 'original-title'.to_sym => sanitize(source_email) }
}
if user.nil?
@@ -166,7 +197,7 @@ module CommitsHelper
link_to(
namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff.new_path)),
- class: 'btn btn-small view-file js-view-file'
+ class: 'btn view-file js-view-file'
) do
raw('View file @') + content_tag(:span, commit_sha[0..6],
class: 'commit-short-id')
@@ -180,4 +211,15 @@ module CommitsHelper
def clean(string)
Sanitize.clean(string, remove_contents: true)
end
+
+ def limited_commits(commits)
+ if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
+ [
+ commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE),
+ commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE
+ ]
+ else
+ [commits, 0]
+ end
+ end
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 24134310fc5..ff32e834499 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -1,106 +1,37 @@
module DiffHelper
+ def mark_inline_diffs(old_line, new_line)
+ old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs
+
+ marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs)
+ marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs)
+
+ [marked_old_line, marked_new_line]
+ end
+
def diff_view
params[:view] == 'parallel' ? 'parallel' : 'inline'
end
- def allowed_diff_size
- if diff_hard_limit_enabled?
- Commit::DIFF_HARD_LIMIT_FILES
- else
- Commit::DIFF_SAFE_FILES
- end
+ def diff_hard_limit_enabled?
+ params[:force_show_diff].present?
end
- def allowed_diff_lines
+ def diff_options
+ options = { ignore_whitespace_change: params[:w] == '1' }
if diff_hard_limit_enabled?
- Commit::DIFF_HARD_LIMIT_LINES
- else
- Commit::DIFF_SAFE_LINES
+ options.merge!(Commit.max_diff_options)
end
+ options
end
- 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?
- # Enabling hard limit allows user to see more diff information
- if params[:force_show_diff].present?
- true
- else
- false
- end
+ def safe_diff_files(diffs, diff_refs)
+ diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs) }
end
def generate_line_code(file_path, line)
Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
end
- def parallel_diff(diff_file, index)
- lines = []
- skip_next = false
-
- # Building array of lines
- #
- # [
- # left_type, left_line_number, left_line_content, left_line_code,
- # right_line_type, right_line_number, right_line_content, right_line_code
- # ]
- #
- diff_file.diff_lines.each do |line|
-
- full_line = line.text
- type = line.type
- line_code = generate_line_code(diff_file.file_path, line)
- line_new = line.new_pos
- line_old = line.old_pos
-
- next_line = diff_file.next_line(line.index)
-
- if next_line
- next_line_code = generate_line_code(diff_file.file_path, next_line)
- next_type = next_line.type
- next_line = next_line.text
- end
-
- if type == 'match' || type.nil?
- # line in the right panel is the same as in the left one
- line = [type, line_old, full_line, line_code, type, line_new, full_line, line_code]
- lines.push(line)
- elsif type == 'old'
- if next_type == 'new'
- # Left side has text removed, right side has text added
- line = [type, line_old, full_line, line_code, next_type, line_new, next_line, next_line_code]
- lines.push(line)
- skip_next = true
- elsif next_type == 'old' || next_type.nil?
- # Left side has text removed, right side doesn't have any change
- # No next line code, no new line number, no new line text
- line = [type, line_old, full_line, line_code, next_type, nil, "&nbsp;", nil]
- lines.push(line)
- end
- elsif type == 'new'
- if skip_next
- # Change has been already included in previous line so no need to do it again
- skip_next = false
- next
- else
- # Change is only on the right side, left side has no change
- line = [nil, nil, "&nbsp;", line_code, type, line_new, full_line, line_code]
- lines.push(line)
- end
- end
- end
- lines
- end
-
def unfold_bottom_class(bottom)
(bottom) ? 'js-unfold-bottom' : ''
end
@@ -111,14 +42,14 @@ module DiffHelper
def diff_line_content(line)
if line.blank?
- " &nbsp;"
+ " &nbsp;".html_safe
else
line
end
end
def line_comments
- @line_comments ||= @line_notes.select(&:active?).group_by(&:line_code)
+ @line_comments ||= @line_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code)
end
def organize_comments(type_left, type_right, line_code_left, line_code_right)
@@ -160,8 +91,7 @@ module DiffHelper
def commit_for_diff(diff)
if diff.deleted_file
- first_commit = @first_commit || @commit
- first_commit.parent || @first_commit
+ @base_commit || @commit.parent || @commit
else
@commit
end
@@ -187,7 +117,7 @@ module DiffHelper
# Always use HTML to handle case where JSON diff rendered this button
params_copy.delete(:format)
- link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn') do
+ link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn'), data: { view_type: name } do
title
end
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
new file mode 100644
index 00000000000..74f326e0b83
--- /dev/null
+++ b/app/helpers/dropdowns_helper.rb
@@ -0,0 +1,100 @@
+module DropdownsHelper
+ def dropdown_tag(toggle_text, options: {}, &block)
+ content_tag :div, class: "dropdown" do
+ data_attr = { toggle: "dropdown" }
+
+ if options.has_key?(:data)
+ data_attr = options[:data].merge(data_attr)
+ end
+
+ dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
+
+ dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do
+ output = ""
+
+ if options.has_key?(:title)
+ output << dropdown_title(options[:title])
+ end
+
+ if options.has_key?(:filter)
+ output << dropdown_filter(options[:placeholder])
+ end
+
+ output << content_tag(:div, class: "dropdown-content") do
+ capture(&block) if block && !options.has_key?(:footer_content)
+ end
+
+ if block && options.has_key?(:footer_content)
+ output << content_tag(:div, class: "dropdown-footer") do
+ capture(&block)
+ end
+ end
+
+ output << dropdown_loading
+
+ output.html_safe
+ end
+
+ dropdown_output.html_safe
+ end
+ end
+
+ def dropdown_toggle(toggle_text, data_attr, options)
+ content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
+ output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
+ output << icon('chevron-down')
+ output.html_safe
+ end
+ end
+
+ def dropdown_title(title, back: false)
+ content_tag :div, class: "dropdown-title" do
+ title_output = ""
+
+ if back
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
+ icon('arrow-left')
+ end
+ end
+
+ title_output << content_tag(:span, title)
+
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
+ icon('times')
+ end
+
+ title_output.html_safe
+ end
+ end
+
+ def dropdown_filter(placeholder)
+ content_tag :div, class: "dropdown-input" do
+ filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
+ filter_output << icon('search')
+
+ filter_output.html_safe
+ end
+ end
+
+ def dropdown_content(&block)
+ content_tag(:div, class: "dropdown-content") do
+ if block
+ capture(&block)
+ end
+ end
+ end
+
+ def dropdown_footer(&block)
+ content_tag(:div, class: "dropdown-footer") do
+ if block
+ capture(&block)
+ end
+ end
+ end
+
+ def dropdown_loading
+ content_tag :div, class: "dropdown-loading" do
+ icon('spinner spin')
+ end
+ end
+end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index dde83ff36b5..a67a6b208e2 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -3,7 +3,7 @@ module EventsHelper
author = event.author
if author
- link_to author.name, user_path(author.username)
+ link_to author.name, user_path(author.username), title: h(author.name)
else
event.author_name
end
@@ -27,13 +27,15 @@ module EventsHelper
key = key.to_s
active = 'active' if @event_filter.active?(key)
link_opts = {
- class: "event-filter-link btn btn-default #{active}",
+ class: "event-filter-link",
id: "#{key}_event_filter",
title: "Filter by #{tooltip.downcase}",
}
- link_to request.path, link_opts do
- content_tag(:span, ' ' + tooltip)
+ content_tag :li, class: active do
+ link_to request.path, link_opts do
+ content_tag(:span, ' ' + tooltip)
+ end
end
end
@@ -157,7 +159,7 @@ module EventsHelper
link_to(
namespace_project_commit_path(event.project.namespace, event.project,
event.note_commit_id,
- anchor: dom_id(event.target)),
+ anchor: dom_id(event.target), title: h(event.target_title)),
class: "commit_short_id"
) do
"#{event.note_target_type} #{event.note_short_commit_id}"
@@ -165,12 +167,12 @@ module EventsHelper
elsif event.note_project_snippet?
link_to(namespace_project_snippet_path(event.project.namespace,
event.project,
- event.note_target)) do
- "#{event.note_target_type} ##{truncate event.note_target_id}"
+ event.note_target), title: h(event.project.name)) do
+ "#{event.note_target_type} #{truncate event.note_target.to_reference}"
end
else
link_to event_note_target_path(event) do
- "#{event.note_target_type} ##{truncate event.note_target_iid}"
+ "#{event.note_target_type} #{truncate event.note_target.to_reference}"
end
end
else
@@ -192,7 +194,7 @@ module EventsHelper
end
def event_to_atom(xml, event)
- if event.proper?
+ if event.proper?(current_user)
xml.entry do
event_link = event_feed_url(event)
event_title = event_feed_title(event)
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
index 0d291f9a87e..337b0aacbb5 100644
--- a/app/helpers/explore_helper.rb
+++ b/app/helpers/explore_helper.rb
@@ -1,5 +1,5 @@
module ExploreHelper
- def explore_projects_filter_path(options={})
+ def filter_projects_path(options={})
exist_opts = {
sort: params[:sort],
scope: params[:scope],
@@ -9,9 +9,12 @@ module ExploreHelper
}
options = exist_opts.merge(options)
-
- path = explore_projects_path
+ path = request.path
path << "?#{options.to_param}"
path
end
+
+ def explore_controller?
+ controller.class.name.split("::").first == "Explore"
+ end
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index ca41657cec1..2f760af02fd 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -50,6 +50,8 @@ module GitlabMarkdownHelper
context[:project] ||= @project
+ text = Banzai.pre_process(text, context)
+
html = Banzai.render(text, context)
context.merge!(
@@ -78,6 +80,21 @@ module GitlabMarkdownHelper
)
end
+ def other_markup(file_name, text)
+ Gitlab::OtherMarkup.render(
+ file_name,
+ text,
+ project: @project,
+ current_user: (current_user if defined?(current_user)),
+
+ # RelativeLinkFilter
+ project_wiki: @project_wiki,
+ requested_path: @path,
+ ref: @ref,
+ commit: @commit
+ )
+ end
+
# Return the first line of +text+, up to +max_chars+, after parsing the line
# as Markdown. HTML tags in the parsed output are not counted toward the
# +max_chars+ limit. If the length limit falls within a tag's contents, then
@@ -91,7 +108,7 @@ module GitlabMarkdownHelper
def render_wiki_content(wiki_page)
case wiki_page.format
when :markdown
- markdown(wiki_page.content)
+ markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki)
when :asciidoc
asciidoc(wiki_page.content)
else
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 5724d3aabec..ab3ef454e1c 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -7,7 +7,16 @@ module IconsHelper
# font-awesome-rails gem, but should we ever use a different icon pack in the
# future we won't have to change hundreds of method calls.
def icon(names, options = {})
- fa_icon(names, options)
+ options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
+ end
+
+ def audit_icon(names, options = {})
+ case names
+ when "standard"
+ names = "key"
+ end
+
+ options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
end
def spinner(text = nil, visible = false)
@@ -37,7 +46,7 @@ module IconsHelper
else # Gitlab::VisibilityLevel::PUBLIC
'globe'
end
-
+
name << " fw" if fw
icon(name)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
new file mode 100644
index 00000000000..81df2094392
--- /dev/null
+++ b/app/helpers/issuables_helper.rb
@@ -0,0 +1,58 @@
+module IssuablesHelper
+
+ def sidebar_gutter_toggle_icon
+ sidebar_gutter_collapsed? ? icon('angle-double-left') : icon('angle-double-right')
+ end
+
+ def sidebar_gutter_collapsed_class
+ "right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
+ end
+
+ def issuables_count(issuable)
+ base_issuable_scope(issuable).maximum(:iid)
+ end
+
+ def next_issuable_for(issuable)
+ base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
+ end
+
+ def prev_issuable_for(issuable)
+ base_issuable_scope(issuable).where('iid < ?', issuable.iid).first
+ end
+
+ def user_dropdown_label(user_id, default_label)
+ return "Unassigned" if user_id == "0"
+
+ if @project
+ member = @project.team.find_member(user_id)
+ user = member.user if member
+ else
+ user = User.find_by(id: user_id)
+ end
+
+ if user
+ user.name
+ else
+ default_label
+ end
+ end
+
+ private
+
+ def sidebar_gutter_collapsed?
+ cookies[:collapsed_gutter] == 'true'
+ end
+
+ def base_issuable_scope(issuable)
+ issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable))
+ end
+
+ def issuable_state_scope(issuable)
+ if issuable.respond_to?(:merged?) && issuable.merged?
+ :merged
+ else
+ issuable.open? ? :opened : :closed
+ end
+ end
+
+end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 80e2741b09a..e00d3204027 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -44,14 +44,14 @@ module IssuesHelper
end
def bulk_update_milestone_options
- milestones = project_active_milestones.to_a
+ milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(Milestone::None)
options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id])
end
def milestone_options(object)
- milestones = object.project.milestones.active.to_a
+ milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(Milestone::None)
options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id)
@@ -69,7 +69,7 @@ module IssuesHelper
end
end
- def issue_button_visibility(issue, closed)
+ def issue_button_visibility(issue, closed)
return 'hidden' if issue.closed? == closed
end
@@ -80,7 +80,7 @@ module IssuesHelper
xml.link href: namespace_project_issue_url(issue.project.namespace,
issue.project, issue)
xml.title truncate(issue.title, length: 80)
- xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ")
+ xml.updated issue.created_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email))
xml.author do |author|
xml.name issue.author_name
@@ -98,14 +98,21 @@ module IssuesHelper
end.sort.to_sentence(last_word_connector: ', or ')
end
+ def confidential_icon(issue)
+ icon('eye-slash') if issue.confidential?
+ end
+
def emoji_icon(name, unicode = nil, aliases = [])
- unicode ||= Emoji.emoji_filename(name)
+ unicode ||= Emoji.emoji_filename(name) rescue ""
content_tag :div, "",
class: "icon emoji-icon emoji-#{unicode}",
- "data-emoji" => name,
- "data-aliases" => aliases.join(" "),
- "data-unicode-name" => unicode
+ title: name,
+ data: {
+ aliases: aliases.join(' '),
+ emoji: name,
+ unicode_name: unicode
+ }
end
def emoji_author_list(notes, current_user)
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index a2c3d4d2f32..4455dcd0e20 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -7,6 +7,8 @@ module LabelsHelper
# project - Project object which will be used as the context for the label's
# link. If omitted, defaults to `@project`, or the label's own
# project.
+ # type - The type of item the link will point to (:issue or
+ # :merge_request). If omitted, defaults to :issue.
# block - An optional block that will be passed to `link_to`, forming the
# body of the link element. If omitted, defaults to
# `render_colored_label`.
@@ -23,14 +25,19 @@ module LabelsHelper
# # Force the generated link to use a provided project
# link_to_label(label, project: Project.last)
#
+ # # Force the generated link to point to merge requests instead of issues
+ # link_to_label(label, type: :merge_request)
+ #
# # Customize link body with a block
# link_to_label(label) { "My Custom Label Text" }
#
# Returns a String
- def link_to_label(label, project: nil, &block)
+ def link_to_label(label, project: nil, type: :issue, &block)
project ||= @project || label.project
- link = namespace_project_issues_path(project.namespace, project,
- label_name: label.name)
+ link = send("namespace_project_#{type.to_s.pluralize}_path",
+ project.namespace,
+ project,
+ label_name: label.name)
if block_given?
link_to link, &block
@@ -43,19 +50,25 @@ module LabelsHelper
@project.labels.pluck(:title)
end
- def render_colored_label(label)
+ def render_colored_label(label, label_suffix = '')
label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color)
# 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>'
+ %(style="background-color: #{label_color}; color: #{text_color}">) +
+ %(#{escape_once(label.name)}#{label_suffix}</span>)
span.html_safe
end
+ def render_colored_cross_project_label(label)
+ label_suffix = label.project.name_with_namespace
+ label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
+ render_colored_label(label, label_suffix)
+ end
+
def suggested_colors
[
'#0033CC',
@@ -83,7 +96,11 @@ module LabelsHelper
end
def text_color_for_bg(bg_color)
- r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex)
+ if bg_color.length == 4
+ r, g, b = bg_color[1, 4].scan(/./).map { |v| (v * 2).hex }
+ else
+ r, g, b = bg_color[1, 7].scan(/.{2}/).map(&:hex)
+ end
if (r + g + b) > 500
'#333333'
@@ -107,6 +124,15 @@ module LabelsHelper
options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
end
+ def label_subscription_status(label)
+ label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
+ end
+
+ def label_subscription_toggle_button_text(label)
+ label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+ end
+
# Required for Banzai::Filter::LabelReferenceFilter
- module_function :render_colored_label, :text_color_for_bg, :escape_once
+ module_function :render_colored_label, :render_colored_cross_project_label,
+ :text_color_for_bg, :escape_once
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index a42cbcff182..92ed0891e92 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -9,10 +9,36 @@ module MilestonesHelper
end
end
+ def milestones_label_path(opts = {})
+ if @project
+ namespace_project_issues_path(@project.namespace, @project, opts)
+ elsif @group
+ issues_group_path(@group, opts)
+ else
+ issues_dashboard_path(opts)
+ end
+ end
+
+ def milestones_browse_issuables_path(milestone, type:)
+ opts = { milestone_title: milestone.title }
+
+ if @project
+ polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts)
+ elsif @group
+ polymorphic_url([type, @group], opts)
+ else
+ polymorphic_url([type, :dashboard], opts)
+ end
+ end
+
+ def milestone_issues_by_label_count(milestone, label, state:)
+ milestone.issues.with_label(label.title).send(state).size
+ end
+
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
- style: "width: #{milestone.percent_complete}%;"
+ style: "width: #{milestone.percent_complete(current_user)}%;"
}
content_tag :div, class: 'progress' do
@@ -33,7 +59,18 @@ module MilestonesHelper
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any)
+ grouped_milestones.unshift(Milestone::Upcoming)
options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
end
+
+ def milestone_remaining_days(milestone)
+ if milestone.expired?
+ content_tag(:strong, 'expired')
+ elsif milestone.due_date
+ days = milestone.remaining_days
+ content = content_tag(:strong, days)
+ content << " #{'day'.pluralize(days)} remaining"
+ end
+ end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index e6fb8670e57..5d86bd490a8 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -19,6 +19,20 @@ module NavHelper
end
end
+ def page_gutter_class
+ if current_path?('merge_requests#show') ||
+ current_path?('merge_requests#diffs') ||
+ current_path?('merge_requests#commits') ||
+ current_path?('merge_requests#builds') ||
+ current_path?('issues#show')
+ if cookies[:collapsed_gutter] == 'true'
+ "page-gutter right-sidebar-collapsed"
+ else
+ "page-gutter right-sidebar-expanded"
+ end
+ end
+ end
+
def nav_header_class
if nav_menu_collapsed?
"header-collapsed"
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 5f0c921413a..53c543c28c5 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -67,7 +67,7 @@ module NotesHelper
line_type: line_type
}
- button_tag class: 'btn reply-btn js-discussion-reply-button',
+ button_tag class: 'btn btn-nr reply-btn js-discussion-reply-button',
data: data, title: 'Add a reply' do
link_text = icon('comment')
link_text << ' Reply'
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 791cb9e50bd..82f805fa444 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -27,35 +27,20 @@ module PageLayoutHelper
#
# Returns an HTML-safe String.
def page_description(description = nil)
- @page_description ||= page_description_default
-
if description.present?
@page_description = description.squish
- else
+ elsif @page_description.present?
sanitize(@page_description, tags: []).truncate_words(30)
end
end
- # Default value for page_description when one hasn't been defined manually by
- # a view
- def page_description_default
- if @project
- @project.description || brand_title
- else
- brand_title
- end
- end
-
def page_image
default = image_url('gitlab_logo.png')
- if @project
- @project.avatar_url || default
- elsif @user
- avatar_icon(@user)
- else
- default
- end
+ subject = @project || @user || @group
+
+ image = subject.avatar_url if subject.present?
+ image || default
end
# Define or get attributes to be used as Twitter card metadata
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 77ba612548a..b5acb80b720 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -8,7 +8,7 @@ module ProjectsHelper
end
def link_to_project(project)
- link_to [project.namespace.becomes(Namespace), project] do
+ link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
if project.namespace
@@ -20,6 +20,12 @@ module ProjectsHelper
end
end
+ def link_to_member_avatar(author, opts = {})
+ default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
+ opts = default_opts.merge(opts)
+ image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
+ end
+
def link_to_member(project, author, opts = {})
default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
opts = default_opts.merge(opts)
@@ -32,15 +38,19 @@ module ProjectsHelper
author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
# Build name span tag
- author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
+ if opts[:by_username]
+ author_html << content_tag(:span, sanitize("@#{author.username}"), class: opts[:author_class]) if opts[:name]
+ else
+ author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
+ end
author_html = author_html.html_safe
if opts[:name]
- link_to(author_html, user_path(author), class: "author_link").html_safe
+ link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else
title = opts[:title].sub(":name", sanitize(author.name))
- link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => title, container: 'body' } ).html_safe
+ link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe
end
end
@@ -53,14 +63,23 @@ module ProjectsHelper
link_to(simple_sanitize(owner.name), user_path(owner))
end
- project_link = link_to(simple_sanitize(project.name), project_path(project))
+ project_link = link_to project_path(project), { class: "project-item-select-holder" } do
+ link_output = simple_sanitize(project.name)
+
+ if current_user
+ link_output += project_select_tag :project_path,
+ class: "project-item-select js-projects-dropdown",
+ data: { include_groups: false, order_by: 'last_activity_at' }
+ end
+
+ link_output
+ end
+ project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle" if current_user
full_title = namespace_link + ' / ' + project_link
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
- content_tag :span do
- full_title
- end
+ full_title
end
def remove_project_message(project)
@@ -83,10 +102,6 @@ module ProjectsHelper
project_nav_tabs.include? name
end
- def project_active_milestones
- @project.milestones.active.order("due_date, title ASC")
- end
-
def project_for_deploy_key(deploy_key)
if deploy_key.projects.include?(@project)
@project
@@ -116,7 +131,7 @@ module ProjectsHelper
private
def get_project_nav_tabs(project, current_user)
- nav_tabs = [:home]
+ nav_tabs = [:home, :forks]
if !project.empty_repo? && can?(current_user, :download_code, project)
nav_tabs << [:files, :commits, :network, :graphs]
@@ -126,7 +141,7 @@ module ProjectsHelper
nav_tabs << :merge_requests
end
- if project.builds_enabled? && can?(current_user, :read_build, project)
+ if can?(current_user, :read_build, project)
nav_tabs << :builds
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index a6ee6880247..494dad0b41e 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -40,7 +40,7 @@ module SearchHelper
{ label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") },
{ label: "help: SSH Keys Help", url: help_page_path("ssh", "README") },
{ label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
- { label: "help: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") },
+ { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
{ label: "help: Workflow Help", url: help_page_path("workflow", "README") },
]
end
@@ -70,7 +70,7 @@ module SearchHelper
# Autocomplete results for the current user's groups
def groups_autocomplete(term, limit = 5)
- GroupsFinder.new.execute(current_user).search(term).limit(limit).map do |group|
+ current_user.authorized_groups.search(term).limit(limit).map do |group|
{
label: "group: #{search_result_sanitize(group.name)}",
url: group_path(group)
@@ -80,7 +80,7 @@ module SearchHelper
# Autocomplete results for the current user's projects
def projects_autocomplete(term, limit = 5)
- ProjectsFinder.new.execute(current_user).search_by_title(term).
+ current_user.authorized_projects.search_by_title(term).
sorted_by_stars.non_archived.limit(limit).map do |p|
{
label: "project: #{search_result_sanitize(p.name_with_namespace)}",
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 906cb12cd48..0a5a8eb5aee 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -1,14 +1,4 @@
module SnippetsHelper
- def lifetime_select_options
- options = [
- ['forever', nil],
- ['1 day', "#{Date.current + 1.day}"],
- ['1 week', "#{Date.current + 1.week}"],
- ['1 month', "#{Date.current + 1.month}"]
- ]
- options_for_select(options)
- end
-
def reliable_snippet_path(snippet)
if snippet.project_id?
namespace_project_snippet_path(snippet.project.namespace,
@@ -17,4 +7,79 @@ module SnippetsHelper
snippet_path(snippet)
end
end
+
+ # Get an array of line numbers surrounding a matching
+ # line, bounded by min/max.
+ #
+ # @returns Array of line numbers
+ def bounded_line_numbers(line, min, max, surrounding_lines)
+ lower = line - surrounding_lines > min ? line - surrounding_lines : min
+ upper = line + surrounding_lines < max ? line + surrounding_lines : max
+ (lower..upper).to_a
+ end
+
+ # Returns a sorted set of lines to be included in a snippet preview.
+ # This ensures matching adjacent lines do not display duplicated
+ # surrounding code.
+ #
+ # @returns Array, unique and sorted.
+ def matching_lines(lined_content, surrounding_lines, query)
+ used_lines = []
+ lined_content.each_with_index do |line, line_number|
+ used_lines.concat bounded_line_numbers(
+ line_number,
+ 0,
+ lined_content.size,
+ surrounding_lines
+ ) if line.include?(query)
+ end
+
+ used_lines.uniq.sort
+ end
+
+ # 'Chunkify' entire snippet. Splits the snippet data into matching lines +
+ # surrounding_lines() worth of unmatching lines.
+ #
+ # @returns a hash with {snippet_object, snippet_chunks:{data,start_line}}
+ def chunk_snippet(snippet, query, surrounding_lines = 3)
+ lined_content = snippet.content.split("\n")
+ used_lines = matching_lines(lined_content, surrounding_lines, query)
+
+ snippet_chunk = []
+ snippet_chunks = []
+ snippet_start_line = 0
+ last_line = -1
+
+ # Go through each used line, and add consecutive lines as a single chunk
+ # to the snippet chunk array.
+ used_lines.each do |line_number|
+ if last_line < 0
+ # Start a new chunk.
+ snippet_start_line = line_number
+ snippet_chunk << lined_content[line_number]
+ elsif last_line == line_number - 1
+ # Consecutive line, continue chunk.
+ snippet_chunk << lined_content[line_number]
+ else
+ # Non-consecutive line, add chunk to chunk array.
+ snippet_chunks << {
+ data: snippet_chunk.join("\n"),
+ start_line: snippet_start_line + 1
+ }
+
+ # Start a new chunk.
+ snippet_chunk = [lined_content[line_number]]
+ snippet_start_line = line_number
+ end
+ last_line = line_number
+ end
+ # Add final chunk to chunk array
+ snippet_chunks << {
+ data: snippet_chunk.join("\n"),
+ start_line: snippet_start_line + 1
+ }
+
+ # Return snippet with chunk array
+ { snippet_object: snippet, snippet_chunks: snippet_chunks }
+ end
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index bb12d43f397..2f2d2721d6d 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -11,6 +11,18 @@ module SortingHelper
sort_value_largest_repo => sort_title_largest_repo,
sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin,
+ sort_value_downvotes => sort_title_downvotes,
+ sort_value_upvotes => sort_title_upvotes
+ }
+ end
+
+ def projects_sort_options_hash
+ {
+ sort_value_name => sort_title_name,
+ sort_value_recently_updated => sort_title_recently_updated,
+ sort_value_oldest_updated => sort_title_oldest_updated,
+ sort_value_recently_created => sort_title_recently_created,
+ sort_value_oldest_created => sort_title_oldest_created,
}
end
@@ -19,7 +31,7 @@ module SortingHelper
end
def sort_title_recently_updated
- 'Recently updated'
+ 'Last updated'
end
def sort_title_oldest_created
@@ -27,7 +39,7 @@ module SortingHelper
end
def sort_title_recently_created
- 'Recently created'
+ 'Last created'
end
def sort_title_milestone_soon
@@ -54,6 +66,14 @@ module SortingHelper
'Oldest sign in'
end
+ def sort_title_downvotes
+ 'Least popular'
+ end
+
+ def sort_title_upvotes
+ 'Most popular'
+ end
+
def sort_value_oldest_updated
'updated_asc'
end
@@ -63,11 +83,11 @@ module SortingHelper
end
def sort_value_oldest_created
- 'created_asc'
+ 'id_asc'
end
def sort_value_recently_created
- 'created_desc'
+ 'id_desc'
end
def sort_value_milestone_soon
@@ -93,4 +113,12 @@ module SortingHelper
def sort_value_oldest_signin
'oldest_sign_in'
end
+
+ def sort_value_downvotes
+ 'downvotes_desc'
+ end
+
+ def sort_value_upvotes
+ 'upvotes_desc'
+ end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
new file mode 100644
index 00000000000..07ddc691d85
--- /dev/null
+++ b/app/helpers/todos_helper.rb
@@ -0,0 +1,87 @@
+module TodosHelper
+ def todos_pending_count
+ current_user.todos.pending.count
+ end
+
+ def todos_done_count
+ current_user.todos.done.count
+ end
+
+ def todo_action_name(todo)
+ case todo.action
+ when Todo::ASSIGNED then 'assigned you'
+ when Todo::MENTIONED then 'mentioned you on'
+ end
+ end
+
+ def todo_target_link(todo)
+ target = todo.target_type.titleize.downcase
+ link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo), { title: h(todo.target.title) }
+ end
+
+ def todo_target_path(todo)
+ anchor = dom_id(todo.note) if todo.note.present?
+
+ polymorphic_path([todo.project.namespace.becomes(Namespace),
+ todo.project, todo.target], anchor: anchor)
+ end
+
+ def todos_filter_params
+ {
+ state: params[:state],
+ project_id: params[:project_id],
+ author_id: params[:author_id],
+ type: params[:type],
+ action_id: params[:action_id],
+ }
+ end
+
+ def todos_filter_path(options = {})
+ without = options.delete(:without)
+
+ options = todos_filter_params.merge(options)
+
+ if without.present?
+ without.each do |key|
+ options.delete(key)
+ end
+ end
+
+ path = request.path
+ path << "?#{options.to_param}"
+ path
+ end
+
+ def todo_actions_options
+ actions = [
+ OpenStruct.new(id: '', title: 'Any Action'),
+ OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'),
+ OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned')
+ ]
+
+ options_from_collection_for_select(actions, 'id', 'title', params[:action_id])
+ end
+
+ def todo_projects_options
+ projects = current_user.authorized_projects.sorted_by_activity.non_archived
+ projects = projects.includes(:namespace)
+
+ projects = projects.map do |project|
+ OpenStruct.new(id: project.id, title: project.name_with_namespace)
+ end
+
+ projects.unshift(OpenStruct.new(id: '', title: 'Any Project'))
+
+ options_from_collection_for_select(projects, 'id', 'title', params[:project_id])
+ end
+
+ def todo_types_options
+ types = [
+ OpenStruct.new(title: 'Any Type', name: ''),
+ OpenStruct.new(title: 'Issue', name: 'Issue'),
+ OpenStruct.new(title: 'Merge Request', name: 'MergeRequest')
+ ]
+
+ options_from_collection_for_select(types, 'name', 'title', params[:type])
+ end
+end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 2ad7c80dae0..4920ca5af6e 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -56,8 +56,7 @@ module TreeHelper
return false unless on_top_of_branch?(project, ref)
- can?(current_user, :push_code, project) ||
- (current_user && current_user.already_forked?(project))
+ can_collaborate_with_project?(project)
end
def tree_edit_branch(project = @project, ref = @ref)
diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb
index f0c41f69a5c..d0ce827a595 100644
--- a/app/mailers/abuse_report_mailer.rb
+++ b/app/mailers/abuse_report_mailer.rb
@@ -2,11 +2,19 @@ class AbuseReportMailer < BaseMailer
include Gitlab::CurrentSettings
def notify(abuse_report_id)
+ return unless deliverable?
+
@abuse_report = AbuseReport.find(abuse_report_id)
mail(
- to: current_application_settings.admin_notification_email,
+ to: current_application_settings.admin_notification_email,
subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse"
)
end
+
+ private
+
+ def deliverable?
+ current_application_settings.admin_notification_email.present?
+ end
end
diff --git a/app/mailers/email_rejection_mailer.rb b/app/mailers/email_rejection_mailer.rb
index 883f1c73ad4..76db31a4c45 100644
--- a/app/mailers/email_rejection_mailer.rb
+++ b/app/mailers/email_rejection_mailer.rb
@@ -10,7 +10,7 @@ class EmailRejectionMailer < BaseMailer
subject: "[Rejected] #{@original_message.subject}"
}
- headers['Message-ID'] = SecureRandom.hex
+ headers['Message-ID'] = "<#{SecureRandom.hex}@#{Gitlab.config.gitlab.host}>"
headers['In-Reply-To'] = @original_message.message_id
headers['References'] = @original_message.message_id
diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb
index d58609a2de5..2f86d1be576 100644
--- a/app/mailers/emails/builds.rb
+++ b/app/mailers/emails/builds.rb
@@ -3,13 +3,27 @@ module Emails
def build_fail_email(build_id, to)
@build = Ci::Build.find(build_id)
@project = @build.project
+
+ add_project_headers
+ add_build_headers('failed')
mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
end
def build_success_email(build_id, to)
@build = Ci::Build.find(build_id)
@project = @build.project
+
+ add_project_headers
+ add_build_headers('success')
mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
end
+
+ private
+
+ def add_build_headers(status)
+ headers['X-GitLab-Build-Id'] = @build.id
+ headers['X-GitLab-Build-Ref'] = @build.ref
+ headers['X-GitLab-Build-Status'] = status.to_s
+ end
end
end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index abdeefed5ef..5f9adb32e00 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -1,35 +1,51 @@
module Emails
module Issues
def new_issue_email(recipient_id, issue_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
end
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- @updated_by = User.find updated_by_user_id
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+ end
+
+ def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id)
+ setup_issue_mail(issue_id, recipient_id)
+
+ @label_names = label_names
+ @labels_url = namespace_project_labels_url(@project.namespace, @project)
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- @issue_status = status
- @updated_by = User.find updated_by_user_id
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ @issue_status = status
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
private
+ def setup_issue_mail(issue_id, recipient_id)
+ @issue = Issue.find(issue_id)
+ @project = @issue.project
+ @target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
+
+ @sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
+ end
+
def issue_thread_options(sender_id, recipient_id)
{
from: sender(sender_id),
@@ -37,15 +53,5 @@ module Emails
subject: subject("#{@issue.title} (##{@issue.iid})")
}
end
-
- def issue_mail_with_notification(issue_id, recipient_id)
- @issue = Issue.find(issue_id)
- @project = @issue.project
- @target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
-
- yield
-
- SentNotification.record(@issue, recipient_id, reply_key)
- end
end
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 7923fb770d0..55bb4f65270 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -1,77 +1,63 @@
module Emails
module MergeRequests
def new_merge_request_email(recipient_id, merge_request_id)
- @merge_request = MergeRequest.find(merge_request_id)
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
- mail_new_thread(@merge_request,
- from: sender(@merge_request.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ setup_merge_request_mail(merge_request_id, recipient_id)
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
+ end
+
+ def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ @label_names = label_names
+ @labels_url = namespace_project_labels_url(@project.namespace, @project)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
- @updated_by = User.find updated_by_user_id
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ setup_merge_request_mail(merge_request_id, recipient_id)
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ setup_merge_request_mail(merge_request_id, recipient_id)
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
@mr_status = status
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
+ end
+
+ private
+
+ def setup_merge_request_mail(merge_request_id, recipient_id)
+ @merge_request = MergeRequest.find(merge_request_id)
@project = @merge_request.project
- @updated_by = User.find updated_by_user_id
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ @target_url = namespace_project_merge_request_url(@project.namespace, @project, @merge_request)
+
+ @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
+ end
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ def merge_request_thread_options(sender_id, recipient_id)
+ {
+ from: sender(sender_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@merge_request.title} (##{@merge_request.iid})")
+ }
end
end
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 65f37e92677..f9650df9a74 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -1,31 +1,31 @@
module Emails
module Notes
def note_commit_email(recipient_id, note_id)
- note_mail_with_notification(note_id, recipient_id) do
- @commit = @note.noteable
- @target_url = namespace_project_commit_url(*note_target_url_options)
-
- mail_answer_thread(@commit,
- from: sender(@note.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@commit.title} (#{@commit.short_id})"))
- end
+ setup_note_mail(note_id, recipient_id)
+
+ @commit = @note.noteable
+ @target_url = namespace_project_commit_url(*note_target_url_options)
+
+ mail_answer_thread(@commit,
+ from: sender(@note.author_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@commit.title} (#{@commit.short_id})"))
end
def note_issue_email(recipient_id, note_id)
- note_mail_with_notification(note_id, recipient_id) do
- @issue = @note.noteable
- @target_url = namespace_project_issue_url(*note_target_url_options)
- mail_answer_thread(@issue, note_thread_options(recipient_id))
- end
+ setup_note_mail(note_id, recipient_id)
+
+ @issue = @note.noteable
+ @target_url = namespace_project_issue_url(*note_target_url_options)
+ mail_answer_thread(@issue, note_thread_options(recipient_id))
end
def note_merge_request_email(recipient_id, note_id)
- note_mail_with_notification(note_id, recipient_id) do
- @merge_request = @note.noteable
- @target_url = namespace_project_merge_request_url(*note_target_url_options)
- mail_answer_thread(@merge_request, note_thread_options(recipient_id))
- end
+ setup_note_mail(note_id, recipient_id)
+
+ @merge_request = @note.noteable
+ @target_url = namespace_project_merge_request_url(*note_target_url_options)
+ mail_answer_thread(@merge_request, note_thread_options(recipient_id))
end
private
@@ -42,13 +42,11 @@ module Emails
}
end
- def note_mail_with_notification(note_id, recipient_id)
+ def setup_note_mail(note_id, recipient_id)
@note = Note.find(note_id)
@project = @note.project
- yield
-
- SentNotification.record(@note, recipient_id, reply_key)
+ @sent_notification = SentNotification.record_note(@note, recipient_id, reply_key)
end
end
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 3a83b083109..256cbcd73a1 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -14,7 +14,10 @@ module Emails
end
def new_ssh_key_email(key_id)
- @key = Key.find(key_id)
+ @key = Key.find_by_id(key_id)
+
+ return unless @key
+
@current_user = @user = @key.user
@target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index b96418679bd..377c2999d6c 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -43,7 +43,7 @@ module Emails
@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,
@@ -65,6 +65,10 @@ module Emails
# used in notify layout
@target_url = @message.target_url
+ @project = Project.find project_id
+
+ add_project_headers
+ headers['X-GitLab-Author'] = @message.author_username
mail(from: sender(@message.author_id, @message.send_from_committer_email?),
reply_to: @message.reply_to,
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 3bbdd9cee76..8cbc9eefc7b 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -100,17 +100,11 @@ class Notify < BaseMailer
end
def mail_thread(model, headers = {})
- if @project
- headers['X-GitLab-Project'] = @project.name
- headers['X-GitLab-Project-Id'] = @project.id
- headers['X-GitLab-Project-Path'] = @project.path_with_namespace
- end
-
+ add_project_headers
headers["X-GitLab-#{model.class.name}-ID"] = model.id
+ headers['X-GitLab-Reply-Key'] = reply_key
- if reply_key
- headers['X-GitLab-Reply-Key'] = reply_key
-
+ if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
@@ -153,4 +147,12 @@ class Notify < BaseMailer
def reply_key
@reply_key ||= SentNotification.reply_key
end
+
+ def add_project_headers
+ return unless @project
+
+ headers['X-GitLab-Project'] = @project.name
+ headers['X-GitLab-Project-Id'] = @project.id
+ headers['X-GitLab-Project-Path'] = @project.path_with_namespace
+ end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 1b3ee757040..e22da4806e6 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -5,17 +5,19 @@ class Ability
return [] unless user.is_a?(User)
return [] if user.blocked?
- case subject.class.name
- when "Project" then project_abilities(user, subject)
- when "Issue" then issue_abilities(user, subject)
- when "Note" then note_abilities(user, subject)
- when "ProjectSnippet" then project_snippet_abilities(user, subject)
- when "PersonalSnippet" then personal_snippet_abilities(user, subject)
- 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 group_member_abilities(user, subject)
- when "ProjectMember" then project_member_abilities(user, subject)
+ case subject
+ when CommitStatus then commit_status_abilities(user, subject)
+ when Project then project_abilities(user, subject)
+ when Issue then issue_abilities(user, subject)
+ when ExternalIssue then external_issue_abilities(user, subject)
+ when Note then note_abilities(user, subject)
+ when ProjectSnippet then project_snippet_abilities(user, subject)
+ when PersonalSnippet then personal_snippet_abilities(user, subject)
+ 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 group_member_abilities(user, subject)
+ when ProjectMember then project_member_abilities(user, subject)
else []
end.concat(global_abilities(user))
end
@@ -25,6 +27,8 @@ class Ability
case true
when subject.is_a?(PersonalSnippet)
anonymous_personal_snippet_abilities(subject)
+ when subject.is_a?(CommitStatus)
+ anonymous_commit_status_abilities(subject)
when subject.is_a?(Project) || subject.respond_to?(:project)
anonymous_project_abilities(subject)
when subject.is_a?(Group) || subject.respond_to?(:group)
@@ -45,23 +49,35 @@ class Ability
rules = [
:read_project,
:read_wiki,
- :read_issue,
:read_label,
:read_milestone,
:read_project_snippet,
:read_project_member,
:read_merge_request,
:read_note,
- :read_build,
+ :read_commit_status,
:download_code
]
+ # Allow to read builds by anonymous user if guests are allowed
+ rules << :read_build if project.public_builds?
+
+ # Allow to read issues by anonymous user if issue is not confidential
+ rules << :read_issue unless subject.is_a?(Issue) && subject.confidential?
+
rules - project_disabled_features_rules(project)
else
[]
end
end
+ def anonymous_commit_status_abilities(subject)
+ rules = anonymous_project_abilities(subject.project)
+ # If subject is Ci::Build which inherits from CommitStatus filter the abilities
+ rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
+ rules
+ end
+
def anonymous_group_abilities(subject)
group = if subject.is_a?(Group)
subject
@@ -69,7 +85,7 @@ class Ability
subject.group
end
- if group && group.public_profile?
+ if group && group.projects.public_only.any?
[:read_group]
else
[]
@@ -95,24 +111,14 @@ class Ability
key = "/user/#{user.id}/project/#{project.id}"
RequestStore.store[key] ||= begin
- team = project.team
-
- # Rules based on role in project
- if team.master?(user)
- rules.push(*project_master_rules)
-
- elsif team.developer?(user)
- rules.push(*project_dev_rules)
-
- elsif team.reporter?(user)
- rules.push(*project_report_rules)
-
- elsif team.guest?(user)
- rules.push(*project_guest_rules)
- end
+ # Push abilities on the users team role
+ rules.push(*project_team_rules(project.team, user))
- if project.public? || project.internal?
+ if project.public? || (project.internal? && !user.external?)
rules.push(*public_project_rules)
+
+ # Allow to read builds for internal projects
+ rules << :read_build if project.public_builds?
end
if project.owner == user || user.admin?
@@ -131,10 +137,24 @@ class Ability
end
end
+ def project_team_rules(team, user)
+ # Rules based on role in project
+ if team.master?(user)
+ project_master_rules
+ elsif team.developer?(user)
+ project_dev_rules
+ elsif team.reporter?(user)
+ project_report_rules
+ elsif team.guest?(user)
+ project_guest_rules
+ end
+ end
+
def public_project_rules
@public_project_rules ||= project_guest_rules + [
:download_code,
- :fork_project
+ :fork_project,
+ :read_commit_status,
]
end
@@ -149,7 +169,6 @@ class Ability
:read_project_member,
:read_merge_request,
:read_note,
- :read_build,
:create_project,
:create_issue,
:create_note
@@ -158,24 +177,27 @@ class Ability
def project_report_rules
@project_report_rules ||= project_guest_rules + [
- :create_commit_status,
- :read_commit_statuses,
:download_code,
:fork_project,
:create_project_snippet,
:update_issue,
:admin_issue,
- :admin_label
+ :admin_label,
+ :read_commit_status,
+ :read_build,
]
end
def project_dev_rules
@project_dev_rules ||= project_report_rules + [
:admin_merge_request,
+ :update_merge_request,
+ :create_commit_status,
+ :update_commit_status,
+ :create_build,
+ :update_build,
:create_merge_request,
:create_wiki,
- :manage_builds,
- :download_build_artifacts,
:push_code
]
end
@@ -194,14 +216,15 @@ class Ability
@project_master_rules ||= project_dev_rules + [
:push_code_to_protected_branches,
:update_project_snippet,
- :update_merge_request,
:admin_milestone,
:admin_project_snippet,
:admin_project_member,
:admin_merge_request,
:admin_note,
:admin_wiki,
- :admin_project
+ :admin_project,
+ :admin_commit_status,
+ :admin_build
]
end
@@ -240,6 +263,10 @@ class Ability
rules += named_abilities('wiki')
end
+ unless project.builds_enabled
+ rules += named_abilities('build')
+ end
+
rules
end
@@ -296,6 +323,7 @@ class Ability
end
rules += project_abilities(user, subject.project)
+ rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue)
rules
end
end
@@ -331,7 +359,7 @@ class Ability
]
end
- if snippet.public? || snippet.internal?
+ if snippet.public? || (snippet.internal? && !user.external?)
rules << :read_personal_snippet
end
@@ -376,6 +404,22 @@ class Ability
rules
end
+ def commit_status_abilities(user, subject)
+ rules = project_abilities(user, subject.project)
+ # If subject is Ci::Build which inherits from CommitStatus filter the abilities
+ rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
+ rules
+ end
+
+ def filter_build_abilities(rules)
+ # If we can't read build we should also not have that
+ # ability when looking at this in context of commit_status
+ %w(read create update admin).each do |rule|
+ rules.delete(:"#{rule}_commit_status") unless rules.include?(:"#{rule}_build")
+ end
+ rules
+ end
+
def abilities
@abilities ||= begin
abilities = Six.new
@@ -384,6 +428,10 @@ class Ability
end
end
+ def external_issue_abilities(user, subject)
+ project_abilities(user, subject.project)
+ end
+
private
def named_abilities(name)
@@ -394,5 +442,17 @@ class Ability
:"admin_#{name}"
]
end
+
+ def filter_confidential_issues_abilities(user, issue, rules)
+ return rules if user.admin? || !issue.confidential?
+
+ unless issue.author == user || issue.assignee == user || issue.project.team.member?(user.id)
+ rules.delete(:admin_issue)
+ rules.delete(:read_issue)
+ rules.delete(:update_issue)
+ end
+
+ rules
+ end
end
end
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 89b3116b9f2..b61f5123127 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -17,5 +17,16 @@ class AbuseReport < ActiveRecord::Base
validates :reporter, presence: true
validates :user, presence: true
validates :message, presence: true
- validates :user_id, uniqueness: true
+ validates :user_id, uniqueness: { message: 'has already been reported' }
+
+ def remove_user(deleted_by:)
+ user.block
+ DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
+ end
+
+ def notify
+ return unless self.persisted?
+
+ AbuseReportMailer.notify(self.id).deliver_later
+ end
end
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
new file mode 100644
index 00000000000..4cf8dd9a8ce
--- /dev/null
+++ b/app/models/appearance.rb
@@ -0,0 +1,9 @@
+class Appearance < ActiveRecord::Base
+ validates :title, presence: true
+ validates :description, presence: true
+ validates :logo, file_size: { maximum: 1.megabyte }
+ validates :header_logo, file_size: { maximum: 1.megabyte }
+
+ mount_uploader :logo, AttachmentUploader
+ mount_uploader :header_logo, AttachmentUploader
+end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index be69d317d73..269056e0e77 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -27,9 +27,23 @@
# admin_notification_email :string(255)
# shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null
-# runners_registration_token :string(255)
-# require_two_factor_authentication :boolean default(TRUE)
+# runners_registration_token :string
+# require_two_factor_authentication :boolean default(FALSE)
# two_factor_grace_period :integer default(48)
+# metrics_enabled :boolean default(FALSE)
+# metrics_host :string default("localhost")
+# metrics_username :string
+# metrics_password :string
+# metrics_pool_size :integer default(16)
+# metrics_timeout :integer default(10)
+# metrics_method_call_threshold :integer default(10)
+# recaptcha_enabled :boolean default(FALSE)
+# recaptcha_site_key :string
+# recaptcha_private_key :string
+# metrics_port :integer default(8089)
+# sentry_enabled :boolean default(FALSE)
+# sentry_dsn :string
+# email_author_in_body :boolean default(FALSE)
#
class ApplicationSetting < ActiveRecord::Base
@@ -57,8 +71,8 @@ class ApplicationSetting < ActiveRecord::Base
url: true
validates :admin_notification_email,
- allow_blank: true,
- email: true
+ email: true,
+ allow_blank: true
validates :two_factor_grace_period,
numericality: { greater_than_or_equal_to: 0 }
@@ -71,6 +85,18 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
if: :recaptcha_enabled
+ validates :sentry_dsn,
+ presence: true,
+ if: :sentry_enabled
+
+ validates :akismet_api_key,
+ presence: true,
+ if: :akismet_enabled
+
+ validates :max_attachment_size,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
@@ -126,7 +152,9 @@ class ApplicationSetting < ActiveRecord::Base
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
- two_factor_grace_period: 48
+ two_factor_grace_period: 48,
+ recaptcha_enabled: false,
+ akismet_enabled: false
)
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
new file mode 100644
index 00000000000..72e6c5fa3fd
--- /dev/null
+++ b/app/models/blob.rb
@@ -0,0 +1,37 @@
+# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
+class Blob < SimpleDelegator
+ CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
+ CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
+
+ # Wrap a Gitlab::Git::Blob object, or return nil when given nil
+ #
+ # This method prevents the decorated object from evaluating to "truthy" when
+ # given a nil value. For example:
+ #
+ # blob = Blob.new(nil)
+ # puts "truthy" if blob # => "truthy"
+ #
+ # blob = Blob.decorate(nil)
+ # puts "truthy" if blob # No output
+ def self.decorate(blob)
+ return if blob.nil?
+
+ new(blob)
+ end
+
+ def svg?
+ text? && language && language.name == 'SVG'
+ end
+
+ def to_partial_path
+ if lfs_pointer?
+ 'download'
+ elsif image? || svg?
+ 'image'
+ elsif text?
+ 'text'
+ else
+ 'download'
+ end
+ end
+end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index ad514706160..8a0a8a4c2a9 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -6,7 +6,6 @@
# message :text not null
# starts_at :datetime
# ends_at :datetime
-# alert_type :integer
# created_at :datetime
# updated_at :datetime
# color :string(255)
@@ -23,7 +22,24 @@ class BroadcastMessage < ActiveRecord::Base
validates :color, allow_blank: true, color: true
validates :font, allow_blank: true, color: true
+ default_value_for :color, '#E75E40'
+ default_value_for :font, '#FFFFFF'
+
def self.current
- where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last
+ Rails.cache.fetch("broadcast_message_current", expires_in: 1.minute) do
+ where("ends_at > :now AND starts_at <= :now", now: Time.zone.now).last
+ end
+ end
+
+ def active?
+ started? && !ended?
+ end
+
+ def started?
+ Time.zone.now >= starts_at
+ end
+
+ def ended?
+ ends_at < Time.zone.now
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3e67b2771c1..7d33838044b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -29,6 +29,10 @@
# target_url :string(255)
# description :string(255)
# artifacts_file :text
+# gl_project_id :integer
+# artifacts_metadata :text
+# erased_by_id :integer
+# erased_at :datetime
#
module Ci
@@ -37,6 +41,7 @@ module Ci
belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
+ belongs_to :erased_by, class_name: 'User'
serialize :options
@@ -48,12 +53,15 @@ module Ci
scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) }
mount_uploader :artifacts_file, ArtifactUploader
+ mount_uploader :artifacts_metadata, ArtifactUploader
acts_as_taggable
# To prevent db load megabytes of data from trace
default_scope -> { select(Ci::Build.columns_without_lazy) }
+ before_destroy { project }
+
class << self
def columns_without_lazy
(column_names - LAZY_ATTRIBUTES).map do |column_name|
@@ -97,29 +105,36 @@ module Ci
end
state_machine :status, initial: :pending do
- after_transition pending: :running do |build, transition|
+ after_transition pending: :running do |build|
build.execute_hooks
end
- after_transition any => [:success, :failed, :canceled] do |build, transition|
- return unless build.project
+ # We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed
+ around_transition any => [:success, :failed, :canceled] do |build, block|
+ block.call
+ build.commit.create_next_builds(build) if build.commit
+ end
+ after_transition any => [:success, :failed, :canceled] do |build|
build.update_coverage
- build.commit.create_next_builds(build)
build.execute_hooks
end
end
- def ignored?
- failed? && allow_failure?
- end
-
def retryable?
project.builds_enabled? && commands.present?
end
def retried?
- !self.commit.latest_builds_for_ref(self.ref).include?(self)
+ !self.commit.latest_statuses_for_ref(self.ref).include?(self)
+ end
+
+ def depends_on_builds
+ # Get builds of the same type
+ latest_builds = self.commit.builds.similar(self).latest
+
+ # Return builds from previous stages
+ latest_builds.where('stage_idx < ?', stage_idx)
end
def trace_html
@@ -145,10 +160,6 @@ module Ci
end
end
- def project
- commit.project
- end
-
def project_id
commit.project.id
end
@@ -169,6 +180,7 @@ module Ci
end
def update_coverage
+ return unless project
coverage_regex = project.build_coverage_regex
return unless coverage_regex
coverage = extract_coverage(trace, coverage_regex)
@@ -193,6 +205,10 @@ module Ci
end
end
+ def has_trace?
+ raw_trace.present?
+ end
+
def raw_trace
if File.file?(path_to_trace)
File.read(path_to_trace)
@@ -207,7 +223,7 @@ module Ci
def trace
trace = raw_trace
- if project && trace.present?
+ if project && trace.present? && project.runners_token.present?
trace.gsub(project.runners_token, 'xxxxxx')
else
trace
@@ -291,25 +307,6 @@ module Ci
project.valid_runners_token? token
end
- def target_url
- Gitlab::Application.routes.url_helpers.
- namespace_project_build_url(project.namespace, project, self)
- end
-
- def cancel_url
- if active?
- Gitlab::Application.routes.url_helpers.
- cancel_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
- def retry_url
- if retryable?
- Gitlab::Application.routes.url_helpers.
- retry_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
def can_be_served?(runner)
(tag_list - runner.tag_list).empty?
end
@@ -318,24 +315,55 @@ module Ci
project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
end
- def show_warning?
+ def stuck?
pending? && !any_runners_online?
end
- def download_url
- if artifacts_file.exists?
- Gitlab::Application.routes.url_helpers.
- download_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
def execute_hooks
+ return unless project
build_data = Gitlab::BuildDataBuilder.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
end
+ def artifacts?
+ artifacts_file.exists?
+ end
+
+ def artifacts_metadata?
+ artifacts? && artifacts_metadata.exists?
+ end
+
+ def artifacts_metadata_entry(path, **options)
+ Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry
+ end
+
+ def erase(opts = {})
+ return false unless erasable?
+
+ remove_artifacts_file!
+ remove_artifacts_metadata!
+ erase_trace!
+ update_erased!(opts[:erased_by])
+ end
+ def erasable?
+ complete? && (artifacts? || has_trace?)
+ end
+
+ def erased?
+ !self.erased_at.nil?
+ end
+
+ private
+
+ def erase_trace!
+ self.trace = nil
+ end
+
+ def update_erased!(user = nil)
+ self.update(erased_by: user, erased_at: Time.now)
+ end
private
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
index d2a29236942..f4cf7034b14 100644
--- a/app/models/ci/commit.rb
+++ b/app/models/ci/commit.rb
@@ -25,8 +25,6 @@ module Ci
has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
- scope :ordered, -> { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }
-
validates_presence_of :sha
validate :valid_commit_sha
@@ -42,16 +40,6 @@ module Ci
project.id
end
- def last_build
- builds.order(:id).last
- end
-
- def retry
- latest_builds.each do |build|
- Ci::Build.retry(build)
- end
- end
-
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
@@ -121,12 +109,14 @@ module Ci
@latest_statuses ||= statuses.latest.to_a
end
- def latest_builds
- @latest_builds ||= builds.latest.to_a
+ def latest_statuses_for_ref(ref)
+ latest_statuses.select { |status| status.ref == ref }
end
- def latest_builds_for_ref(ref)
- latest_builds.select { |build| build.ref == ref }
+ def matrix_builds(build = nil)
+ matrix_builds = builds.latest.ordered
+ matrix_builds = matrix_builds.similar(build) if build
+ matrix_builds.to_a
end
def retried
@@ -170,7 +160,7 @@ module Ci
end
def duration
- duration_array = latest_statuses.map(&:duration).compact
+ duration_array = statuses.map(&:duration).compact
duration_array.reduce(:+).to_i
end
@@ -183,16 +173,12 @@ module Ci
end
def coverage
- coverage_array = latest_builds.map(&:coverage).compact
+ coverage_array = latest_statuses.map(&:coverage).compact
if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
end
- def matrix_for_ref?(ref)
- latest_builds_for_ref(ref).size > 1
- end
-
def config_processor
return nil unless ci_yaml_file
@config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
@@ -205,7 +191,11 @@ module Ci
end
def ci_yaml_file
- @ci_yaml_file ||= project.repository.blob_at(sha, '.gitlab-ci.yml').data
+ @ci_yaml_file ||= begin
+ blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
+ blob.load_all_data!(project.repository)
+ blob.data
+ end
rescue
nil
end
@@ -214,10 +204,6 @@ module Ci
git_commit_message =~ /(\[ci skip\])/ if git_commit_message
end
- def update_committed!
- update!(committed_at: DateTime.now)
- end
-
private
def save_yaml_error(error)
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 38b20cd7faa..90349a07594 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -22,7 +22,8 @@ module Ci
extend Ci::Model
LAST_CONTACT_TIME = 5.minutes.ago
-
+ AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online']
+
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id
@@ -38,11 +39,30 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) }
+ scope :owned_or_shared, ->(project_id) do
+ joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
+ .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
+ end
+
acts_as_taggable
+ # Searches for runners matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # This method performs a *partial* match on tokens, thus a query for "a"
+ # will match any runner where the token contains the letter "a". As a result
+ # you should *not* use this method for non-admin purposes as otherwise users
+ # might be able to query a list of all runners.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def self.search(query)
- where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query',
- query: "%#{query.try(:downcase)}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
end
def set_default_values
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 93d9be144e8..7b16f207a26 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -2,11 +2,12 @@
#
# Table name: ci_runner_projects
#
-# id :integer not null, primary key
-# runner_id :integer not null
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
+# id :integer not null, primary key
+# runner_id :integer not null
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# gl_project_id :integer
#
module Ci
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 23516709a41..2b9a457c8ab 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -2,12 +2,13 @@
#
# Table name: ci_triggers
#
-# id :integer not null, primary key
-# token :string(255)
-# project_id :integer not null
-# deleted_at :datetime
-# created_at :datetime
-# updated_at :datetime
+# id :integer not null, primary key
+# token :string(255)
+# project_id :integer
+# deleted_at :datetime
+# created_at :datetime
+# updated_at :datetime
+# gl_project_id :integer
#
module Ci
@@ -32,6 +33,10 @@ module Ci
trigger_requests.last
end
+ def last_used
+ last_trigger_request.try(:created_at)
+ end
+
def short_token
token[0...10]
end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 56759d3e50f..e786bd7dd93 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -3,12 +3,13 @@
# Table name: ci_variables
#
# id :integer not null, primary key
-# project_id :integer not null
+# project_id :integer
# key :string(255)
# value :text
# encrypted_value :text
# encrypted_value_salt :string(255)
# encrypted_value_iv :string(255)
+# gl_project_id :integer
#
module Ci
@@ -17,8 +18,12 @@ module Ci
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
- validates_presence_of :key
validates_uniqueness_of :key, scope: :gl_project_id
+ validates :key,
+ presence: true,
+ length: { within: 0..255 },
+ format: { with: /\A[a-zA-Z0-9_]+\z/,
+ message: "can contain only letters, digits and '_'." }
attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 0ba7b584d91..ce0b85d50cf 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -12,12 +12,7 @@ class Commit
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
- #
- # User can force display of diff above this size
- DIFF_SAFE_FILES = 100 unless defined?(DIFF_SAFE_FILES)
- DIFF_SAFE_LINES = 5000 unless defined?(DIFF_SAFE_LINES)
+ DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
# Commits above this size will not be rendered in HTML
DIFF_HARD_LIMIT_FILES = 1000 unless defined?(DIFF_HARD_LIMIT_FILES)
@@ -36,13 +31,20 @@ class Commit
# Calculate number of lines to render for diffs
def diff_line_count(diffs)
- diffs.reduce(0) { |sum, d| sum + d.diff.lines.count }
+ diffs.reduce(0) { |sum, d| sum + Gitlab::Git::Util.count_lines(d.diff) }
end
# Truncate sha to 8 characters
def truncate_sha(sha)
sha[0..7]
end
+
+ def max_diff_options
+ {
+ max_files: DIFF_HARD_LIMIT_FILES,
+ max_lines: DIFF_HARD_LIMIT_LINES,
+ }
+ end
end
attr_accessor :raw
@@ -68,18 +70,18 @@ class Commit
# Pattern used to extract commit references from text
#
- # The SHA can be between 6 and 40 hex characters.
+ # The SHA can be between 7 and 40 hex characters.
#
# This pattern supports cross-project references.
def self.reference_pattern
%r{
(?:#{Project.reference_pattern}#{reference_prefix})?
- (?<commit>\h{6,40})
+ (?<commit>\h{7,40})
}x
end
def self.link_reference_pattern
- super("commit", /(?<commit>\h{6,40})/)
+ super("commit", /(?<commit>\h{7,40})/)
end
def to_reference(from_project = nil)
@@ -215,6 +217,44 @@ class Commit
ci_commit.try(:status) || :not_found
end
+ def revert_branch_name
+ "revert-#{short_id}"
+ end
+
+ def revert_description
+ if merged_merge_request
+ "This reverts merge request #{merged_merge_request.to_reference}"
+ else
+ "This reverts commit #{sha}"
+ end
+ end
+
+ def revert_message
+ %Q{Revert "#{title}"\n\n#{revert_description}}
+ end
+
+ def reverts_commit?(commit)
+ description? && description.include?(commit.revert_description)
+ end
+
+ def merge_commit?
+ parents.size > 1
+ end
+
+ def merged_merge_request
+ return @merged_merge_request if defined?(@merged_merge_request)
+
+ @merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit?
+ end
+
+ def has_been_reverted?(current_user = nil, noteable = self)
+ Gitlab::ReferenceExtractor.lazily do
+ noteable.notes.system.flat_map do |note|
+ note.all_references(current_user).commits
+ end
+ end.any? { |commit_ref| commit_ref.reverts_commit?(self) }
+ end
+
private
def repo_changes
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 14e7971fa06..289dbc57287 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -32,8 +32,8 @@ class CommitRange
PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/
# In text references, the beginning and ending refs can only be SHAs
- # between 6 and 40 hex characters.
- STRICT_PATTERN = /\h{6,40}\.{2,3}\h{6,40}/
+ # between 7 and 40 hex characters.
+ STRICT_PATTERN = /\h{7,40}\.{2,3}\h{7,40}/
def self.reference_prefix
'@'
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 21c5c87bc3d..3377a85a55a 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,30 +1,35 @@
# == Schema Information
#
-# project_id integer
-# status string
-# finished_at datetime
-# trace text
-# created_at datetime
-# updated_at datetime
-# started_at datetime
-# runner_id integer
-# coverage float
-# commit_id integer
-# commands text
-# job_id integer
-# name string
-# deploy boolean default: false
-# options text
-# allow_failure boolean default: false, null: false
-# stage string
-# trigger_request_id integer
-# stage_idx integer
-# tag boolean
-# ref string
-# user_id integer
-# type string
-# target_url string
-# description string
+# Table name: ci_builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# coverage :float
+# commit_id :integer
+# commands :text
+# job_id :integer
+# name :string(255)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+# stage_idx :integer
+# tag :boolean
+# ref :string(255)
+# user_id :integer
+# type :string(255)
+# target_url :string(255)
+# description :string(255)
+# artifacts_file :text
+# gl_project_id :integer
#
class CommitStatus < ActiveRecord::Base
@@ -51,6 +56,8 @@ class CommitStatus < ActiveRecord::Base
scope :ordered, -> { order(:ref, :stage_idx, :name) }
scope :for_ref, ->(ref) { where(ref: ref) }
+ AVAILABLE_STATUSES = ['pending', 'running', 'success', 'failed', 'canceled']
+
state_machine :status, initial: :pending do
event :run do
transition pending: :running
@@ -68,16 +75,16 @@ class CommitStatus < ActiveRecord::Base
transition [:pending, :running] => :canceled
end
- after_transition pending: :running do |build, transition|
- build.update_attributes started_at: Time.now
+ after_transition pending: :running do |commit_status|
+ commit_status.update_attributes started_at: Time.now
end
- after_transition any => [:success, :failed, :canceled] do |build, transition|
- build.update_attributes finished_at: Time.now
+ after_transition any => [:success, :failed, :canceled] do |commit_status|
+ commit_status.update_attributes finished_at: Time.now
end
- after_transition [:pending, :running] => :success do |build, transition|
- MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.project, nil).trigger(build)
+ after_transition [:pending, :running] => :success do |commit_status|
+ MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status)
end
state :pending, value: 'pending'
@@ -106,6 +113,10 @@ class CommitStatus < ActiveRecord::Base
canceled? || success? || failed?
end
+ def ignored?
+ allow_failure? && (failed? || canceled?)
+ end
+
def duration
if started_at && finished_at
finished_at - started_at
@@ -114,19 +125,7 @@ class CommitStatus < ActiveRecord::Base
end
end
- def cancel_url
- nil
- end
-
- def retry_url
- nil
- end
-
- def show_warning?
+ def stuck?
false
end
-
- def download_url
- nil
- end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 18a00f95b48..86ab84615ba 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -8,6 +8,7 @@ module Issuable
extend ActiveSupport::Concern
include Participable
include Mentionable
+ include Subscribable
include StripAttribute
included do
@@ -18,7 +19,6 @@ 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 }
@@ -29,15 +29,19 @@ module Issuable
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) }
+ scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :opened, -> { with_state(:opened, :reopened) }
scope :only_opened, -> { with_state(:opened) }
scope :only_reopened, -> { with_state(:reopened) }
scope :closed, -> { with_state(:closed) }
scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
+ scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) }
+ scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) }
+ scope :non_archived, -> { join_project.merge(Project.non_archived) }
delegate :name,
:email,
@@ -57,22 +61,64 @@ module Issuable
end
module ClassMethods
+ # Searches for records with a matching title.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("LOWER(title) like :query", query: "%#{query.downcase}%")
+ where(arel_table[:title].matches("%#{query}%"))
end
+ # Searches for records with a matching title or description.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def full_search(query)
- where("LOWER(title) like :query OR LOWER(description) like :query", query: "%#{query.downcase}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
def sort(method)
case method.to_s
when 'milestone_due_asc' then order_milestone_due_asc
when 'milestone_due_desc' then order_milestone_due_desc
+ when 'downvotes_desc' then order_downvotes_desc
+ when 'upvotes_desc' then order_upvotes_desc
else
order_by(method)
end
end
+
+ def order_downvotes_desc
+ order_votes_desc('thumbsdown')
+ end
+
+ def order_upvotes_desc
+ order_votes_desc('thumbsup')
+ end
+
+ def order_votes_desc(award_emoji_name)
+ issuable_table = self.arel_table
+ note_table = Note.arel_table
+
+ join_clause = issuable_table.join(note_table, Arel::Nodes::OuterJoin).on(
+ note_table[:noteable_id].eq(issuable_table[:id]).and(
+ note_table[:noteable_type].eq(self.name).and(
+ note_table[:is_award].eq(true).and(note_table[:note].eq(award_emoji_name))
+ )
+ )
+ ).join_sources
+
+ joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC")
+ end
end
def today?
@@ -103,34 +149,22 @@ module Issuable
notes.awards.where(note: "thumbsup").count
end
- def subscribed?(user)
- subscription = subscriptions.find_by_user_id(user.id)
-
- if subscription
- return subscription.subscribed
- end
-
+ def subscribed_without_subscriptions?(user)
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)
- {
+ hook_data = {
object_kind: self.class.name.underscore,
user: user.hook_attrs,
- repository: {
- name: project.name,
- url: project.url_to_repo,
- description: project.description,
- homepage: project.web_url
- },
- object_attributes: hook_attrs
+ project: project.hook_attrs,
+ object_attributes: hook_attrs,
+ # DEPRECATED
+ repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
}
+ hook_data.merge!(assignee: assignee.hook_attrs) if assignee
+
+ hook_data
end
def label_names
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 6316ee208b5..98f71ae8cb0 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -51,8 +51,11 @@ module Mentionable
else
self.class.mentionable_attrs.each do |attr, options|
text = send(attr)
- options[:cache_key] = [self, attr] if options.delete(:cache) && self.persisted?
- ext.analyze(text, options)
+
+ context = options.dup
+ context[:cache_key] = [self, attr] if context.delete(:cache) && self.persisted?
+
+ ext.analyze(text, context)
end
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
new file mode 100644
index 00000000000..5b8e3f654ea
--- /dev/null
+++ b/app/models/concerns/milestoneish.rb
@@ -0,0 +1,29 @@
+module Milestoneish
+ def closed_items_count(user = nil)
+ issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
+ end
+
+ def total_items_count(user = nil)
+ issues_visible_to_user(user).size + merge_requests.size
+ end
+
+ def complete?(user = nil)
+ total_items_count(user) == closed_items_count(user)
+ end
+
+ def percent_complete(user = nil)
+ ((closed_items_count(user) * 100) / total_items_count(user)).abs
+ rescue ZeroDivisionError
+ 0
+ end
+
+ def remaining_days
+ return 0 if !due_date || expired?
+
+ (due_date - Date.today).to_i
+ end
+
+ def issues_visible_to_user(user = nil)
+ issues.visible_to_user(user)
+ end
+end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 7391a77383c..8b47b9e0abd 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -11,6 +11,7 @@ module Sortable
default_scope { order_id_desc }
scope :order_id_desc, -> { reorder(id: :desc) }
+ scope :order_id_asc, -> { reorder(id: :asc) }
scope :order_created_desc, -> { reorder(created_at: :desc) }
scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
@@ -28,6 +29,8 @@ module Sortable
when 'updated_desc' then order_updated_desc
when 'created_asc' then order_created_asc
when 'created_desc' then order_created_desc
+ when 'id_desc' then order_id_desc
+ when 'id_asc' then order_id_asc
else
all
end
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
new file mode 100644
index 00000000000..d5a881b2445
--- /dev/null
+++ b/app/models/concerns/subscribable.rb
@@ -0,0 +1,44 @@
+# == Subscribable concern
+#
+# Users can subscribe to these models.
+#
+# Used by Issue, MergeRequest, Label
+#
+
+module Subscribable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :subscriptions, dependent: :destroy, as: :subscribable
+ end
+
+ def subscribed?(user)
+ if subscription = subscriptions.find_by_user_id(user.id)
+ subscription.subscribed
+ else
+ subscribed_without_subscriptions?(user)
+ end
+ end
+
+ # Override this method to define custom logic to consider a subscribable as
+ # subscribed without an explicit subscription record.
+ def subscribed_without_subscriptions?(user)
+ false
+ end
+
+ def subscribers
+ subscriptions.where(subscribed: true).map(&:user)
+ end
+
+ def toggle_subscription(user)
+ subscriptions.
+ find_or_initialize_by(user_id: user.id).
+ update(subscribed: !subscribed?(user))
+ end
+
+ def unsubscribe(user)
+ subscriptions.
+ find_or_initialize_by(user_id: user.id).
+ update(subscribed: false)
+ end
+end
diff --git a/app/models/diff_line.rb b/app/models/diff_line.rb
deleted file mode 100644
index ad37945874a..00000000000
--- a/app/models/diff_line.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-class DiffLine
- attr_accessor :type, :content, :num, :code
-end
diff --git a/app/models/email.rb b/app/models/email.rb
index 935705e2ed4..b323d1edd10 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -15,7 +15,7 @@ class Email < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
- validates :email, presence: true, email: { strict_mode: true }, uniqueness: true
+ validates :email, presence: true, uniqueness: true, email: true
validate :unique_email, if: ->(email) { email.email_changed? }
before_validation :cleanup_email
diff --git a/app/models/event.rb b/app/models/event.rb
index 01d008035a5..a5cfeaf388e 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -47,7 +47,11 @@ class Event < ActiveRecord::Base
# Scopes
scope :recent, -> { reorder(id: :desc) }
scope :code_push, -> { where(action: PUSHED) }
- scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent }
+
+ scope :in_projects, ->(projects) do
+ where(project_id: projects.map(&:id)).recent
+ end
+
scope :with_associations, -> { includes(project: :namespace) }
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
@@ -64,26 +68,22 @@ class Event < ActiveRecord::Base
[Event::CREATED, Event::CLOSED, Event::MERGED])
end
- def latest_update_time
- row = select(:updated_at, :project_id).reorder(id: :desc).take
-
- row ? row.updated_at : nil
- end
-
def limit_recent(limit = 20, offset = nil)
recent.limit(limit).offset(offset)
end
end
- def proper?
+ def proper?(user = nil)
if push?
true
elsif membership_changed?
true
elsif created_project?
true
+ elsif issue?
+ Ability.abilities.allowed?(user, :read_issue, issue)
else
- ((issue? || merge_request? || note?) && target) || milestone?
+ ((merge_request? || note?) && target) || milestone?
end
end
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 49f6c95e045..2ca79df0a29 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -31,7 +31,7 @@ class ExternalIssue
# Pattern used to extract `JIRA-123` issue references from text
def self.reference_pattern
- %r{(?<issue>([A-Z\-]+-)\d+)}
+ %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
def to_reference(_from_project = nil)
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
index 12c934e2494..97f4f03a9a5 100644
--- a/app/models/generic_commit_status.rb
+++ b/app/models/generic_commit_status.rb
@@ -29,6 +29,7 @@
# target_url :string(255)
# description :string(255)
# artifacts_file :text
+# gl_project_id :integer
#
class GenericCommitStatus < CommitStatus
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
index 0171f7d54b7..ddd4bad5c21 100644
--- a/app/models/global_label.rb
+++ b/app/models/global_label.rb
@@ -2,16 +2,19 @@ class GlobalLabel
attr_accessor :title, :labels
alias_attribute :name, :title
+ delegate :color, :description, to: :@first_label
+
def self.build_collection(labels)
labels = labels.group_by(&:title)
- labels.map do |title, label|
- new(title, label)
+ labels.map do |title, labels|
+ new(title, labels)
end
end
def initialize(title, labels)
@title = title
@labels = labels
+ @first_label = labels.find { |lbl| lbl.description.present? } || labels.first
end
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index af1d7562ebe..97bd79af083 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -1,4 +1,6 @@
class GlobalMilestone
+ include Milestoneish
+
attr_accessor :title, :milestones
alias_attribute :name, :title
@@ -28,33 +30,7 @@ class GlobalMilestone
end
def projects
- milestones.map { |milestone| milestone.project }
- end
-
- def issue_count
- milestones.map { |milestone| milestone.issues.count }.sum
- end
-
- def merge_requests_count
- milestones.map { |milestone| milestone.merge_requests.count }.sum
- end
-
- def open_items_count
- milestones.map { |milestone| milestone.open_items_count }.sum
- end
-
- def closed_items_count
- milestones.map { |milestone| milestone.closed_items_count }.sum
- end
-
- def total_items_count
- milestones.map { |milestone| milestone.total_items_count }.sum
- end
-
- def percent_complete
- ((closed_items_count * 100) / total_items_count).abs
- rescue ZeroDivisionError
- 0
+ @projects ||= Project.for_milestones(milestones.map(&:id))
end
def state
@@ -76,35 +52,20 @@ class GlobalMilestone
end
def issues
- @issues ||= milestones.map(&:issues).flatten.group_by(&:state)
+ @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project)
end
def merge_requests
- @merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
+ @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project)
end
def participants
@participants ||= milestones.map(&:participants).flatten.compact.uniq
end
- def opened_issues
- issues.values_at("opened", "reopened").compact.flatten
- end
-
- def closed_issues
- issues['closed']
- end
-
- def opened_merge_requests
- merge_requests.values_at("opened", "reopened").compact.flatten
- end
-
- def closed_merge_requests
- merge_requests.values_at("closed", "merged", "locked").compact.flatten
- end
-
- def complete?
- total_items_count == closed_items_count
+ def labels
+ @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten)
+ .sort_by!(&:title)
end
def due_date
@@ -121,9 +82,9 @@ class GlobalMilestone
def expires_at
if due_date
if due_date.past?
- "expired at #{due_date.stamp("Aug 21, 2011")}"
+ "expired on #{due_date.to_s(:medium)}"
else
- "expires at #{due_date.stamp("Aug 21, 2011")}"
+ "expires on #{due_date.to_s(:medium)}"
end
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 1b5b875a19e..9919ca112dc 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -11,7 +11,6 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
-# public :boolean default(FALSE)
#
require 'carrierwave/orm/activerecord'
@@ -20,10 +19,12 @@ require 'file_size_validator'
class Group < Namespace
include Gitlab::ConfigHelper
include Referable
-
+
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, through: :group_members
+ has_many :project_group_links, dependent: :destroy
+ has_many :shared_projects, through: :project_group_links, source: :project
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
@@ -34,8 +35,18 @@ class Group < Namespace
after_destroy :post_destroy_hook
class << self
+ # Searches for groups matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%")
+ table = Namespace.arel_table
+ pattern = "%#{query}%"
+
+ where(table[:name].matches(pattern).or(table[:path].matches(pattern)))
end
def sort(method)
@@ -50,10 +61,6 @@ class Group < Namespace
User.reference_pattern
end
- def public_and_given_groups(ids)
- where('public IS TRUE OR namespaces.id IN (?)', ids)
- end
-
def visible_to_user(user)
where(id: user.authorized_groups.select(:id).reorder(nil))
end
@@ -125,10 +132,6 @@ class Group < Namespace
end
end
- def public_profile?
- self.public || projects.public_only.any?
- end
-
def post_create_hook
Gitlab::AppLogger.info("Group \"#{name}\" was created")
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 22638057773..fe923fafbe0 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -3,11 +3,11 @@
# Table name: web_hooks
#
# id :integer not null, primary key
-# url :string(255)
+# url :string(2000)
# project_id :integer
# created_at :datetime
# updated_at :datetime
-# type :string(255) default("ProjectHook")
+# type :string default("ProjectHook")
# service_id :integer
# push_events :boolean default(TRUE), not null
# issues_events :boolean default(FALSE), not null
@@ -15,6 +15,7 @@
# tag_push_events :boolean default(FALSE)
# note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE)
+# build_events :boolean default(FALSE), not null
#
class ProjectHook < WebHook
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 09bb3ee52a2..80962264ba2 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -3,11 +3,11 @@
# Table name: web_hooks
#
# id :integer not null, primary key
-# url :string(255)
+# url :string(2000)
# project_id :integer
# created_at :datetime
# updated_at :datetime
-# type :string(255) default("ProjectHook")
+# type :string default("ProjectHook")
# service_id :integer
# push_events :boolean default(TRUE), not null
# issues_events :boolean default(FALSE), not null
@@ -15,6 +15,7 @@
# tag_push_events :boolean default(FALSE)
# note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE)
+# build_events :boolean default(FALSE), not null
#
class ServiceHook < WebHook
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index 2f63c59b07e..c147d8762a9 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -3,11 +3,11 @@
# Table name: web_hooks
#
# id :integer not null, primary key
-# url :string(255)
+# url :string(2000)
# project_id :integer
# created_at :datetime
# updated_at :datetime
-# type :string(255) default("ProjectHook")
+# type :string default("ProjectHook")
# service_id :integer
# push_events :boolean default(TRUE), not null
# issues_events :boolean default(FALSE), not null
@@ -15,6 +15,7 @@
# tag_push_events :boolean default(FALSE)
# note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE)
+# build_events :boolean default(FALSE), not null
#
class SystemHook < WebHook
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 40eb0e20b4b..7a13c3f0a39 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -3,11 +3,11 @@
# Table name: web_hooks
#
# id :integer not null, primary key
-# url :string(255)
+# url :string(2000)
# project_id :integer
# created_at :datetime
# updated_at :datetime
-# type :string(255) default("ProjectHook")
+# type :string default("ProjectHook")
# service_id :integer
# push_events :boolean default(TRUE), not null
# issues_events :boolean default(FALSE), not null
@@ -15,6 +15,7 @@
# tag_push_events :boolean default(FALSE)
# note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE)
+# build_events :boolean default(FALSE), not null
#
class WebHook < ActiveRecord::Base
@@ -47,8 +48,8 @@ class WebHook < ActiveRecord::Base
else
post_url = url.gsub("#{parsed_url.userinfo}@", "")
auth = {
- username: URI.decode(parsed_url.user),
- password: URI.decode(parsed_url.password),
+ username: CGI.unescape(parsed_url.user),
+ password: CGI.unescape(parsed_url.password),
}
response = WebHook.post(post_url,
body: data.to_json,
@@ -60,7 +61,7 @@ class WebHook < ActiveRecord::Base
basic_auth: auth)
end
- [response.code == 200, ActionView::Base.full_sanitizer.sanitize(response.to_s)]
+ [(response.code >= 200 && response.code < 300), ActionView::Base.full_sanitizer.sanitize(response.to_s)]
rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
logger.error("WebHook Error => #{e}")
[false, e.to_s]
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 8bcdc194953..e1915b079d4 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -18,4 +18,8 @@ class Identity < ActiveRecord::Base
validates :provider, presence: true
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
validates :user_id, uniqueness: { scope: :provider }
+
+ def ldap?
+ provider.starts_with?('ldap')
+ end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 80ecd15077f..053387cffd7 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -33,9 +33,12 @@ class Issue < ActiveRecord::Base
belongs_to :project
validates :project, presence: true
- scope :of_group, ->(group) { where(project_id: group.project_ids) }
+ scope :of_group,
+ ->(group) { where(project_id: group.projects.select(:id).reorder(nil)) }
+
scope :cared, ->(user) { where(assignee_id: user) }
scope :open_for, ->(user) { opened.assigned_to(user) }
+ scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
state_machine :state, initial: :opened do
event :close do
@@ -55,6 +58,13 @@ class Issue < ActiveRecord::Base
attributes
end
+ def self.visible_to_user(user)
+ return where(confidential: false) if user.blank?
+ return all if user.admin?
+
+ where('issues.confidential = false OR (issues.confidential = true AND (issues.author_id = :user_id OR issues.assignee_id = :user_id OR issues.project_id IN(:project_ids)))', user_id: user.id, project_ids: user.authorized_projects.select(:id))
+ end
+
def self.reference_prefix
'#'
end
@@ -83,12 +93,22 @@ class Issue < ActiveRecord::Base
reference
end
- def referenced_merge_requests
- Gitlab::ReferenceExtractor.lazily do
- [self, *notes].flat_map do |note|
- note.all_references.merge_requests
- end
- end.sort_by(&:iid)
+ def referenced_merge_requests(current_user = nil)
+ @referenced_merge_requests ||= {}
+ @referenced_merge_requests[current_user] ||= begin
+ Gitlab::ReferenceExtractor.lazily do
+ [self, *notes].flat_map do |note|
+ note.all_references(current_user).merge_requests
+ end
+ end.sort_by(&:iid).uniq
+ end
+ end
+
+ def related_branches
+ return [] if self.project.empty_repo?
+ self.project.repository.branch_names.select do |branch|
+ branch =~ /\A#{iid}-(?!\d+-stable)/i
+ end
end
# Reset issue events cache
@@ -117,4 +137,15 @@ class Issue < ActiveRecord::Base
note.all_references(current_user).merge_requests
end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
end
+
+ def to_branch_name
+ "#{iid}-#{title.parameterize}"
+ end
+
+ def can_be_worked_on?(current_user)
+ !self.closed? &&
+ !self.project.forked? &&
+ self.related_branches.empty? &&
+ self.closed_by_merge_requests(current_user).empty?
+ end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index 406a1257b5d..0282ad18139 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -16,6 +16,7 @@
require 'digest/md5'
class Key < ActiveRecord::Base
+ include AfterCommitQueue
include Sortable
belongs_to :user
@@ -62,7 +63,7 @@ class Key < ActiveRecord::Base
end
def notify_user
- NotificationService.new.new_key(self)
+ run_after_commit { NotificationService.new.new_key(self) }
end
def post_create_hook
diff --git a/app/models/label.rb b/app/models/label.rb
index 220da10a6ab..f7ffc0b7f36 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -2,17 +2,20 @@
#
# Table name: labels
#
-# id :integer not null, primary key
-# title :string(255)
-# color :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# template :boolean default(FALSE)
+# id :integer not null, primary key
+# title :string(255)
+# color :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# template :boolean default(FALSE)
+# description :string(255)
#
class Label < ActiveRecord::Base
include Referable
+ include Subscribable
+
# Represents a "No Label" state used for filtering Issues and Merge
# Requests that have no label assigned.
LabelStruct = Struct.new(:title, :name)
@@ -26,6 +29,7 @@ class Label < ActiveRecord::Base
belongs_to :project
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
+ has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? }
@@ -46,10 +50,15 @@ class Label < ActiveRecord::Base
'~'
end
+ ##
# Pattern used to extract label references from text
+ #
+ # This pattern supports cross-project references.
+ #
def self.reference_pattern
%r{
- #{reference_prefix}
+ (#{Project.reference_pattern})?
+ #{Regexp.escape(reference_prefix)}
(?:
(?<label_id>\d+) | # Integer-based label ID, or
(?<label_name>
@@ -60,24 +69,31 @@ class Label < ActiveRecord::Base
}x
end
+ def self.link_reference_pattern
+ nil
+ end
+
+ ##
# Returns the String necessary to reference this Label in Markdown
#
# format - Symbol format to use (default: :id, optional: :name)
#
- # Note that its argument differs from other objects implementing Referable. If
- # a non-Symbol argument is given (such as a Project), it will default to :id.
- #
# Examples:
#
- # Label.first.to_reference # => "~1"
- # Label.first.to_reference(:name) # => "~\"bug\""
+ # Label.first.to_reference # => "~1"
+ # Label.first.to_reference(format: :name) # => "~\"bug\""
+ # Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1"
#
# Returns a String
- def to_reference(format = :id)
- if format == :name && !name.include?('"')
- %(#{self.class.reference_prefix}"#{name}")
+ #
+ def to_reference(from_project = nil, format: :id)
+ format_reference = label_format_reference(format)
+ reference = "#{self.class.reference_prefix}#{format_reference}"
+
+ if cross_project_reference?(from_project)
+ project.to_reference + reference
else
- "#{self.class.reference_prefix}#{id}"
+ reference
end
end
@@ -85,7 +101,27 @@ class Label < ActiveRecord::Base
issues.opened.count
end
+ def closed_issues_count
+ issues.closed.count
+ end
+
+ def open_merge_requests_count
+ merge_requests.opened.count
+ end
+
def template?
template
end
+
+ private
+
+ def label_format_reference(format = :id)
+ raise StandardError, 'Unknown format' unless [:id, :name].include?(format)
+
+ if format == :name && !name.include?('"')
+ %("#{name}")
+ else
+ id
+ end
+ end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 28aee2e3799..ca08007b7eb 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -39,7 +39,6 @@ class Member < ActiveRecord::Base
if: :invite?
},
email: {
- strict_mode: true,
allow_nil: true
},
uniqueness: {
@@ -91,7 +90,7 @@ class Member < ActiveRecord::Base
member.invite_email = user
end
- if can_update_member?(current_user, member)
+ if can_update_member?(current_user, member) || project_creator?(member, access_level)
member.created_by ||= current_user
member.access_level = access_level
@@ -107,6 +106,11 @@ class Member < ActiveRecord::Base
current_user.can?(:update_group_member, member) ||
current_user.can?(:update_project_member, member)
end
+
+ def project_creator?(member, access_level)
+ member.new_record? && member.owner? &&
+ access_level.to_i == ProjectMember::MASTER
+ end
end
def invite?
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 1b0c76917aa..560d1690e14 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -84,7 +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
@@ -133,13 +133,13 @@ class ProjectMember < Member
event_service.join_project(self.project, self.user)
notification_service.new_project_member(self)
end
-
+
super
end
def post_update_hook
if access_level_changed?
- notification_service.update_project_member(self)
+ notification_service.update_project_member(self)
end
super
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index ac25d38eb63..30a7bd47be7 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -2,28 +2,29 @@
#
# Table name: merge_requests
#
-# id :integer not null, primary key
-# target_branch :string(255) not null
-# source_branch :string(255) not null
-# source_project_id :integer not null
-# author_id :integer
-# assignee_id :integer
-# title :string(255)
-# created_at :datetime
-# updated_at :datetime
-# milestone_id :integer
-# state :string(255)
-# merge_status :string(255)
-# target_project_id :integer not null
-# iid :integer
-# description :text
-# position :integer default(0)
-# locked_at :datetime
-# updated_by_id :integer
-# merge_error :string(255)
-# merge_params :text (serialized to hash)
-# merge_when_build_succeeds :boolean default(false), not null
-# merge_user_id :integer
+# id :integer not null, primary key
+# target_branch :string(255) not null
+# source_branch :string(255) not null
+# source_project_id :integer not null
+# author_id :integer
+# assignee_id :integer
+# title :string(255)
+# created_at :datetime
+# updated_at :datetime
+# milestone_id :integer
+# state :string(255)
+# merge_status :string(255)
+# target_project_id :integer not null
+# iid :integer
+# description :text
+# position :integer default(0)
+# locked_at :datetime
+# updated_by_id :integer
+# merge_error :string(255)
+# merge_params :text
+# merge_when_build_succeeds :boolean default(FALSE), not null
+# merge_user_id :integer
+# merge_commit_sha :string
#
require Rails.root.join("app/models/commit")
@@ -47,7 +48,7 @@ class MergeRequest < ActiveRecord::Base
after_create :create_merge_request_diff
after_update :update_merge_request_diff
- delegate :commits, :diffs, :diffs_no_whitespace, to: :merge_request_diff, prefix: nil
+ delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -55,8 +56,7 @@ class MergeRequest < ActiveRecord::Base
# Temporary fields to store compare vars
# when creating new merge request
- attr_accessor :can_be_created, :compare_failed,
- :compare_commits, :compare_diffs
+ attr_accessor :can_be_created, :compare_commits, :compare
state_machine :state, initial: :opened do
event :close do
@@ -131,14 +131,12 @@ class MergeRequest < ActiveRecord::Base
validate :validate_branches
validate :validate_fork
- scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.project_ids) }
+ scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.projects.select(:id).reorder(nil)) }
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
- scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :merged, -> { with_state(:merged) }
- scope :closed, -> { with_state(:closed) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :join_project, -> { joins(:target_project) }
@@ -162,6 +160,24 @@ class MergeRequest < ActiveRecord::Base
super("merge_requests", /(?<merge_request>\d+)/)
end
+ # Returns all the merge requests from an ActiveRecord:Relation.
+ #
+ # This method uses a UNION as it usually operates on the result of
+ # ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries
+ # using multiple sub-queries especially when combined with an OR statement.
+ # UNIONs on the other hand perform much better in these cases.
+ #
+ # relation - An ActiveRecord::Relation that returns a list of Projects.
+ #
+ # Returns an ActiveRecord::Relation.
+ def self.in_projects(relation)
+ source = where(source_project_id: relation).select(:id)
+ target = where(target_project_id: relation).select(:id)
+ union = Gitlab::SQL::Union.new([source, target])
+
+ where("merge_requests.id IN (#{union.to_sql})")
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -180,6 +196,18 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end
+ def diff_size
+ merge_request_diff.size
+ end
+
+ def diff_base_commit
+ if merge_request_diff
+ merge_request_diff.base_commit
+ elsif source_sha
+ self.target_project.merge_base_commit(self.source_sha, self.target_branch)
+ end
+ end
+
def last_commit_short_sha
last_commit.short_id
end
@@ -229,8 +257,10 @@ class MergeRequest < ActiveRecord::Base
end
def check_if_can_be_merged
+ return unless unchecked?
+
can_be_merged =
- project.repository.can_be_merged?(source_sha, target_branch)
+ !broken? && project.repository.can_be_merged?(source_sha, target_branch)
if can_be_merged
mark_as_mergeable
@@ -248,11 +278,15 @@ class MergeRequest < ActiveRecord::Base
end
def work_in_progress?
- !!(title =~ /\A\[?WIP\]?:? /i)
+ !!(title =~ /\A\[?WIP(\]|:| )/i)
end
def mergeable?
- open? && !work_in_progress? && can_be_merged?
+ return false unless open? && !work_in_progress? && !broken?
+
+ check_if_can_be_merged
+
+ can_be_merged?
end
def gitlab_merge_status
@@ -270,7 +304,8 @@ class MergeRequest < ActiveRecord::Base
def can_remove_source_branch?(current_user)
!source_project.protected_branch?(source_branch) &&
!source_project.root_ref?(source_branch) &&
- Ability.abilities.allowed?(current_user, :push_code, source_project)
+ Ability.abilities.allowed?(current_user, :push_code, source_project) &&
+ last_commit == source_project.commit(source_branch)
end
def mr_and_commit_notes
@@ -332,10 +367,10 @@ class MergeRequest < ActiveRecord::Base
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
- issues = commits.flat_map { |c| c.closes_issues(current_user) }
- issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user).
- closed_by_message(description))
- issues.uniq(&:id)
+ messages = commits.map(&:safe_message) << description
+
+ Gitlab::ClosingIssueExtractor.new(project, current_user).
+ closed_by_message(messages.join("\n"))
else
[]
end
@@ -452,6 +487,10 @@ class MergeRequest < ActiveRecord::Base
!source_branch_exists? || !target_branch_exists?
end
+ def broken?
+ self.commits.blank? || branch_missing? || cannot_be_merged?
+ end
+
def can_be_merged_by?(user)
::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch)
end
@@ -466,13 +505,26 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def state_icon_name
+ if merged?
+ "check"
+ elsif closed?
+ "times"
+ else
+ "circle-o"
+ end
+ end
+
def target_sha
- @target_sha ||= target_project.
- repository.commit(target_branch).sha
+ @target_sha ||= target_project.repository.commit(target_branch).sha
end
def source_sha
- commits.first.sha
+ last_commit.try(:sha) || source_tip.try(:sha)
+ end
+
+ def source_tip
+ source_branch && source_project.repository.commit(source_branch)
end
def fetch_ref
@@ -504,11 +556,44 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def diverged_commits_count
+ cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
+
+ if cache.blank? || cache[:source_sha] != source_sha || cache[:target_sha] != target_sha
+ cache = {
+ source_sha: source_sha,
+ target_sha: target_sha,
+ diverged_commits_count: compute_diverged_commits_count
+ }
+ Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache)
+ end
+
+ cache[:diverged_commits_count]
+ end
+
+ def compute_diverged_commits_count
+ Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size
+ end
+
+ def diverged_from_target_branch?
+ diverged_commits_count > 0
+ end
+
def ci_commit
@ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project
end
- def broken?
- self.commits.blank? || branch_missing? || cannot_be_merged?
+ def diff_refs
+ return nil unless diff_base_commit
+
+ [diff_base_commit, last_commit]
+ end
+
+ def merge_commit
+ @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
+ end
+
+ def can_be_reverted?(current_user = nil)
+ merge_commit && !merge_commit.has_been_reverted?(current_user, self)
end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index c499a4b5b4c..33884118595 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -17,9 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Sortable
# Prevent store of diff if commits amount more then 500
- COMMITS_SAFE_SIZE = 500
-
- attr_reader :commits, :diffs, :diffs_no_whitespace
+ COMMITS_SAFE_SIZE = 100
belongs_to :merge_request
@@ -27,6 +25,9 @@ class MergeRequestDiff < ActiveRecord::Base
state_machine :state, initial: :empty do
state :collected
+ state :overflow
+ # Deprecated states: these are no longer used but these values may still occur
+ # in the database.
state :timeout
state :overflow_commits_safe_size
state :overflow_diff_files_limit
@@ -43,22 +44,23 @@ class MergeRequestDiff < ActiveRecord::Base
reload_diffs
end
- def diffs
- @diffs ||= (load_diffs(st_diffs) || [])
+ def size
+ real_size.presence || diffs.size
end
- def diffs_no_whitespace
- # Get latest sha of branch from source project
- source_sha = merge_request.source_project.commit(source_branch).sha
-
- compare_result = Gitlab::CompareResult.new(
- Gitlab::Git::Compare.new(
- merge_request.target_project.repository.raw_repository,
- merge_request.target_branch,
- source_sha,
- ), { ignore_whitespace_change: true }
- )
- @diffs_no_whitespace ||= load_diffs(dump_commits(compare_result.diffs))
+ def diffs(options={})
+ if options[:ignore_whitespace_change]
+ @diffs_no_whitespace ||= begin
+ compare = Gitlab::Git::Compare.new(
+ self.repository.raw_repository,
+ self.target_branch,
+ self.source_sha,
+ )
+ compare.diffs(options)
+ end
+ else
+ @diffs ||= load_diffs(st_diffs, options)
+ end
end
def commits
@@ -73,12 +75,16 @@ class MergeRequestDiff < ActiveRecord::Base
commits.last
end
+ def base_commit
+ return nil unless self.base_commit_sha
+
+ merge_request.target_project.commit(self.base_commit_sha)
+ end
+
def last_commit_short_sha
@last_commit_short_sha ||= last_commit.short_id
end
- private
-
def dump_commits(commits)
commits.map(&:to_hash)
end
@@ -93,16 +99,18 @@ class MergeRequestDiff < ActiveRecord::Base
end
end
- def load_diffs(raw)
- if raw.respond_to?(:map)
- raw.map { |hash| Gitlab::Git::Diff.new(hash) }
+ def load_diffs(raw, options)
+ if raw.respond_to?(:each)
+ Gitlab::Git::DiffCollection.new(raw, options)
+ else
+ Gitlab::Git::DiffCollection.new([])
end
end
# Collect array of Git::Commit objects
# between target and source branches
def unmerged_commits
- commits = compare_result.commits
+ commits = compare.commits
if commits.present?
commits = Commit.decorate(commits, merge_request.source_project).
@@ -132,64 +140,55 @@ class MergeRequestDiff < ActiveRecord::Base
if commits.size.zero?
self.state = :empty
- elsif commits.size > COMMITS_SAFE_SIZE
- self.state = :overflow_commits_safe_size
else
- new_diffs = unmerged_diffs
- end
+ diff_collection = unmerged_diffs
- if new_diffs.any?
- if new_diffs.size > Commit::DIFF_HARD_LIMIT_FILES
- self.state = :overflow_diff_files_limit
- new_diffs = new_diffs.first(Commit::DIFF_HARD_LIMIT_LINES)
+ if diff_collection.overflow?
+ # Set our state to 'overflow' to make the #empty? and #collected?
+ # methods (generated by StateMachine) return false.
+ self.state = :overflow
end
- if new_diffs.sum { |diff| diff.diff.lines.count } > Commit::DIFF_HARD_LIMIT_LINES
- self.state = :overflow_diff_lines_limit
- new_diffs = new_diffs.first(Commit::DIFF_HARD_LIMIT_LINES)
- end
- end
+ self.real_size = diff_collection.real_size
- if new_diffs.present?
- new_diffs = dump_commits(new_diffs)
- self.state = :collected
+ if diff_collection.any?
+ new_diffs = dump_diffs(diff_collection)
+ self.state = :collected
+ end
end
self.st_diffs = new_diffs
+
+ self.base_commit_sha = self.repository.merge_base(self.source_sha, self.target_branch)
+
self.save
end
# Collect array of Git::Diff objects
# between target and source branches
def unmerged_diffs
- compare_result.diffs || []
- rescue Gitlab::Git::Diff::TimeoutError
- self.state = :timeout
- []
+ compare.diffs(Commit.max_diff_options)
end
def repository
merge_request.target_project.repository
end
- private
+ def source_sha
+ source_commit = merge_request.source_project.commit(source_branch)
+ source_commit.try(:sha)
+ end
- def compare_result
- @compare_result ||=
+ def compare
+ @compare ||=
begin
# Update ref for merge request
merge_request.fetch_ref
- # Get latest sha of branch from source project
- source_commit = merge_request.source_project.commit(source_branch)
- source_sha = source_commit.try(:sha)
-
- Gitlab::CompareResult.new(
- Gitlab::Git::Compare.new(
- merge_request.target_project.repository.raw_repository,
- merge_request.target_branch,
- source_sha,
- )
+ Gitlab::Git::Compare.new(
+ self.repository.raw_repository,
+ self.target_branch,
+ self.source_sha
)
end
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index d8c7536cd31..de7183bf6b4 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -19,21 +19,25 @@ class Milestone < ActiveRecord::Base
MilestoneStruct = Struct.new(:title, :name, :id)
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1)
+ Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include InternalId
include Sortable
+ include Referable
include StripAttribute
+ include Milestoneish
belongs_to :project
has_many :issues
+ has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
- has_many :participants, through: :issues, source: :assignee
+ has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee
scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) }
scope :of_projects, ->(ids) { where(project_id: ids) }
- validates :title, presence: true
+ validates :title, presence: true, uniqueness: { scope: :project_id }
validates :project, presence: true
strip_attributes :title
@@ -55,44 +59,60 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title
class << self
+ # Searches for milestones matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- query = "%#{query}%"
- where("title like ? or description like ?", query, query)
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
end
- def expired?
- if due_date
- due_date.past?
- else
- false
- end
+ def self.reference_pattern
+ nil
+ end
+
+ def self.link_reference_pattern
+ super("milestones", /(?<milestone>\d+)/)
end
- def open_items_count
- self.issues.opened.count + self.merge_requests.opened.count
+ def self.upcoming
+ self.where('due_date > ?', Time.now).order(due_date: :asc).first
end
- def closed_items_count
- self.issues.closed.count + self.merge_requests.closed_and_merged.count
+ def to_reference(from_project = nil)
+ escaped_title = self.title.gsub("]", "\\]")
+
+ h = Gitlab::Application.routes.url_helpers
+ url = h.namespace_project_milestone_url(self.project.namespace, self.project, self)
+
+ "[#{escaped_title}](#{url})"
end
- def total_items_count
- self.issues.count + self.merge_requests.count
+ def reference_link_text(from_project = nil)
+ self.title
end
- def percent_complete
- ((closed_items_count * 100) / total_items_count).abs
- rescue ZeroDivisionError
- 0
+ def expired?
+ if due_date
+ due_date.past?
+ else
+ false
+ end
end
def expires_at
if due_date
if due_date.past?
- "expired at #{due_date.stamp("Aug 21, 2011")}"
+ "expired on #{due_date.to_s(:medium)}"
else
- "expires at #{due_date.stamp("Aug 21, 2011")}"
+ "expires on #{due_date.to_s(:medium)}"
end
end
end
@@ -101,8 +121,8 @@ class Milestone < ActiveRecord::Base
active? && issues.opened.count.zero?
end
- def is_empty?
- total_items_count.zero?
+ def is_empty?(user = nil)
+ total_items_count(user).zero?
end
def author_id
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index adafabbec07..55842df1e2d 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -11,7 +11,6 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
-# public :boolean default(FALSE)
#
class Namespace < ActiveRecord::Base
@@ -53,8 +52,18 @@ class Namespace < ActiveRecord::Base
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
end
+ # Searches for namespaces matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation
def search(query)
- where("name LIKE :query OR path LIKE :query", query: "%#{query}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
end
def clean_path(path)
diff --git a/app/models/note.rb b/app/models/note.rb
index 3d5b663c99f..b0c33f2eec5 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -33,14 +33,18 @@ class Note < ActiveRecord::Base
participant :author
belongs_to :project
- belongs_to :noteable, polymorphic: true
+ belongs_to :noteable, polymorphic: true, touch: true
belongs_to :author, class_name: "User"
belongs_to :updated_by, class_name: "User"
+ has_many :todos, dependent: :destroy
+
+ delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true
delegate :name, :email, to: :author, prefix: true
before_validation :set_award!
+ before_validation :clear_blank_line_code!
validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
@@ -60,7 +64,7 @@ class Note < ActiveRecord::Base
scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") }
- scope :not_inline, ->{ where(line_code: [nil, '']) }
+ scope :not_inline, ->{ where(line_code: nil) }
scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) }
@@ -85,7 +89,7 @@ class Note < ActiveRecord::Base
next if discussion_ids.include?(note.discussion_id)
# don't group notes for the main target
- if !note.for_diff_line? && note.noteable_type == "MergeRequest"
+ if !note.for_diff_line? && note.for_merge_request?
discussions << [note]
else
discussions << notes.select do |other_note|
@@ -102,8 +106,18 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym
end
+ # Searches for notes matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String.
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("LOWER(note) like :query", query: "%#{query.downcase}%")
+ table = arel_table
+ pattern = "%#{query}%"
+
+ where(table[:note].matches(pattern))
end
def grouped_awards
@@ -129,9 +143,11 @@ class Note < ActiveRecord::Base
end
def find_diff
- return nil unless noteable && noteable.diffs.present?
+ return nil unless noteable
+ return @diff if defined?(@diff)
- @diff ||= noteable.diffs.find do |d|
+ # Don't use ||= because nil is a valid value for @diff
+ @diff = noteable.diffs(Commit.max_diff_options).find do |d|
Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path
end
end
@@ -157,30 +173,29 @@ class Note < ActiveRecord::Base
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
+ # Check if this note is part of an "active" discussion
+ #
+ # This will always return true for anything except MergeRequest noteables,
+ # which have special logic.
+ #
+ # If the note's current diff cannot be matched in the MergeRequest's current
+ # diff, it's considered inactive.
def active?
return true unless self.diff
return false unless noteable
+ return @active if defined?(@active)
- noteable.diffs.each do |mr_diff|
- next unless mr_diff.new_path == self.diff.new_path
+ noteable_diff = find_noteable_diff
- lines = Gitlab::Diff::Parser.new.parse(mr_diff.diff.lines.to_a)
+ if noteable_diff
+ parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line)
- lines.each do |line|
- if line.text == diff_line
- return true
- end
- end
+ @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
+ else
+ @active = false
end
- false
- end
-
- def outdated?
- !active?
+ @active
end
def diff_file_index
@@ -244,7 +259,7 @@ class Note < ActiveRecord::Base
prev_match_line = nil
prev_lines = []
- diff_lines.each do |line|
+ highlighted_diff_lines.each do |line|
if line.type == "match"
prev_lines.clear
prev_match_line = line
@@ -261,7 +276,11 @@ class Note < ActiveRecord::Base
end
def diff_lines
- @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.lines.to_a)
+ @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
+ end
+
+ def highlighted_diff_lines
+ Gitlab::Diff::Highlight.new(diff_lines).highlight
end
def discussion_id
@@ -309,20 +328,6 @@ class Note < ActiveRecord::Base
nil
end
- # Mentionable override.
- def gfm_reference(from_project = nil)
- noteable.gfm_reference(from_project)
- end
-
- # Mentionable override.
- def local_reference
- noteable
- end
-
- def noteable_type_name
- noteable_type.downcase if noteable_type.present?
- end
-
# FIXME: Hack for polymorphic associations with STI
# For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
def noteable_type=(noteable_type)
@@ -342,10 +347,6 @@ class Note < ActiveRecord::Base
Event.reset_event_cache_for(self)
end
- def system?
- read_attribute(:system)
- end
-
def downvote?
is_award && note == "thumbsdown"
end
@@ -358,6 +359,10 @@ class Note < ActiveRecord::Base
!system? && !is_award
end
+ def cross_reference_not_visible_for?(user)
+ cross_reference? && referenced_mentionables(user).empty?
+ end
+
# Checks if note is an award added as a comment
#
# If note is an award, this method sets is_award to true
@@ -367,14 +372,25 @@ class Note < ActiveRecord::Base
#
def set_award!
return unless awards_supported? && contains_emoji_only?
+
self.is_award = true
self.note = award_emoji_name
end
private
+ def clear_blank_line_code!
+ self.line_code = nil if self.line_code.blank?
+ end
+
+ # Find the diff on noteable that matches our own
+ def find_noteable_diff
+ diffs = noteable.diffs(Commit.max_diff_options)
+ diffs.find { |d| d.new_path == self.diff.new_path }
+ end
+
def awards_supported?
- noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)
+ (for_issue? || for_merge_request?) && !for_diff_line?
end
def contains_emoji_only?
diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb
index 9cee3b70cb3..452f3913eef 100644
--- a/app/models/personal_snippet.rb
+++ b/app/models/personal_snippet.rb
@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
-# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
diff --git a/app/models/project.rb b/app/models/project.rb
index 017471995ec..412c6c6732d 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -29,6 +29,14 @@
# import_source :string(255)
# commit_count :integer default(0)
# import_error :text
+# ci_id :integer
+# builds_enabled :boolean default(TRUE), not null
+# shared_runners_enabled :boolean default(TRUE), not null
+# runners_token :string
+# build_coverage_regex :string
+# build_allow_git_fetch :boolean default(TRUE), not null
+# build_timeout :integer default(3600), not null
+# pending_delete :boolean
#
require 'carrierwave/orm/activerecord'
@@ -43,6 +51,7 @@ class Project < ActiveRecord::Base
include Sortable
include AfterCommitQueue
include CaseSensitivity
+ include TokenAuthenticatable
extend Gitlab::ConfigHelper
@@ -81,6 +90,7 @@ class Project < ActiveRecord::Base
acts_as_taggable_on :tags
attr_accessor :new_default_branch
+ attr_accessor :old_path_with_namespace
# Relations
belongs_to :creator, foreign_key: 'creator_id', class_name: 'User'
@@ -141,6 +151,9 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects
+ has_many :project_group_links, dependent: :destroy
+ has_many :invited_groups, through: :project_group_links, source: :group
+ has_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
@@ -185,10 +198,8 @@ class Project < ActiveRecord::Base
if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
- before_validation :set_runners_token_token
- def set_runners_token_token
- self.runners_token = SecureRandom.hex(15) if self.runners_token.blank?
- end
+ add_authentication_token_field :runners_token
+ before_save :ensure_runners_token
mount_uploader :avatar, AvatarUploader
@@ -207,6 +218,7 @@ class Project < ActiveRecord::Base
scope :public_only, -> { where(visibility_level: Project::PUBLIC) }
scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) }
scope :non_archived, -> { where(archived: false) }
+ scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
state_machine :import_status, initial: :none do
event :import_start do
@@ -242,12 +254,6 @@ class Project < ActiveRecord::Base
where('projects.last_activity_at < ?', 6.months.ago)
end
- def publicish(user)
- visibility_levels = [Project::PUBLIC]
- visibility_levels << Project::INTERNAL if user
- where(visibility_level: visibility_levels)
- end
-
def with_push
joins(:events).where('events.action = ?', Event::PUSHED)
end
@@ -256,17 +262,49 @@ class Project < ActiveRecord::Base
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end
+ # Searches for a list of projects based on the query given in `query`.
+ #
+ # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
+ # search. On MySQL a regular "LIKE" is used as it's already
+ # case-insensitive.
+ #
+ # query - The search query as a String.
def search(query)
- joins(:namespace).
- where('LOWER(projects.name) LIKE :query OR
- LOWER(projects.path) LIKE :query OR
- LOWER(namespaces.name) LIKE :query OR
- LOWER(projects.description) LIKE :query',
- query: "%#{query.try(:downcase)}%")
+ ptable = arel_table
+ ntable = Namespace.arel_table
+ pattern = "%#{query}%"
+
+ projects = select(:id).where(
+ ptable[:path].matches(pattern).
+ or(ptable[:name].matches(pattern)).
+ or(ptable[:description].matches(pattern))
+ )
+
+ # We explicitly remove any eager loading clauses as they're:
+ #
+ # 1. Not needed by this query
+ # 2. Combined with .joins(:namespace) lead to all columns from the
+ # projects & namespaces tables being selected, leading to a SQL error
+ # due to the columns of all UNION'd queries no longer being the same.
+ namespaces = select(:id).
+ except(:includes).
+ joins(:namespace).
+ where(ntable[:name].matches(pattern))
+
+ union = Gitlab::SQL::Union.new([projects, namespaces])
+
+ where("projects.id IN (#{union.to_sql})")
+ end
+
+ def search_by_visibility(level)
+ where(visibility_level: Gitlab::VisibilityLevel.const_get(level.upcase))
end
def search_by_title(query)
- where('projects.archived = ?', false).where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%")
+ pattern = "%#{query}%"
+ table = Project.arel_table
+
+ non_archived.where(table[:name].matches(pattern))
end
def find_with_namespace(id)
@@ -330,13 +368,18 @@ class Project < ActiveRecord::Base
end
def repository
- @repository ||= Repository.new(path_with_namespace, nil, self)
+ @repository ||= Repository.new(path_with_namespace, self)
end
def commit(id = 'HEAD')
repository.commit(id)
end
+ def merge_base_commit(first_commit_id, second_commit_id)
+ sha = repository.merge_base(first_commit_id, second_commit_id)
+ repository.commit(sha) if sha
+ end
+
def saved?
id && persisted?
end
@@ -365,6 +408,10 @@ class Project < ActiveRecord::Base
external_import? || forked?
end
+ def no_import?
+ import_status == 'none'
+ end
+
def external_import?
import_url.present?
end
@@ -390,7 +437,7 @@ class Project < ActiveRecord::Base
result.password = '*****' unless result.password.nil?
result.to_s
rescue
- original_url
+ self.import_url
end
def check_limit
@@ -461,12 +508,10 @@ class Project < ActiveRecord::Base
!external_issue_tracker
end
- def external_issues_trackers
- services.select(&:issue_tracker?).reject(&:default?)
- end
-
def external_issue_tracker
- @external_issues_tracker ||= external_issues_trackers.find(&:activated?)
+ return @external_issue_tracker if defined?(@external_issue_tracker)
+ @external_issue_tracker ||=
+ services.issue_trackers.active.without_defaults.first
end
def can_have_issues_tracker_id?
@@ -508,11 +553,11 @@ class Project < ActiveRecord::Base
end
def ci_services
- services.select { |service| service.category == :ci }
+ services.where(category: :ci)
end
def ci_service
- @ci_service ||= ci_services.find(&:activated?)
+ @ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
def jira_tracker?
@@ -526,10 +571,7 @@ class Project < ActiveRecord::Base
end
def avatar_in_git
- @avatar_file ||= 'logo.png' if repository.blob_at_branch('master', 'logo.png')
- @avatar_file ||= 'logo.jpg' if repository.blob_at_branch('master', 'logo.jpg')
- @avatar_file ||= 'logo.gif' if repository.blob_at_branch('master', 'logo.gif')
- @avatar_file
+ repository.avatar
end
def avatar_url
@@ -693,6 +735,8 @@ class Project < ActiveRecord::Base
old_path_with_namespace = File.join(namespace_dir, path_was)
new_path_with_namespace = File.join(namespace_dir, path)
+ expire_caches_before_rename(old_path_with_namespace)
+
if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace)
# If repository moved successfully we need to send update instructions to users.
# However we cannot allow rollback since we moved repository
@@ -701,6 +745,11 @@ class Project < ActiveRecord::Base
gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
send_move_instructions(old_path_with_namespace)
reset_events_cache
+
+ @old_path_with_namespace = old_path_with_namespace
+
+ SystemHooksService.new.execute_hooks_for(self, :rename)
+
@repository = nil
rescue
# Returning false does not rollback after_* transaction but gives
@@ -716,14 +765,39 @@ class Project < ActiveRecord::Base
Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path)
end
+ # Expires various caches before a project is renamed.
+ def expire_caches_before_rename(old_path)
+ repo = Repository.new(old_path, self)
+ wiki = Repository.new("#{old_path}.wiki", self)
+
+ if repo.exists?
+ repo.expire_cache
+ repo.expire_emptiness_caches
+ end
+
+ if wiki.exists?
+ wiki.expire_cache
+ wiki.expire_emptiness_caches
+ end
+ end
+
def hook_attrs
{
name: name,
- ssh_url: ssh_url_to_repo,
- http_url: http_url_to_repo,
+ description: description,
web_url: web_url,
+ avatar_url: avatar_url,
+ git_ssh_url: ssh_url_to_repo,
+ git_http_url: http_url_to_repo,
namespace: namespace.name,
- visibility_level: visibility_level
+ visibility_level: visibility_level,
+ path_with_namespace: path_with_namespace,
+ default_branch: default_branch,
+ # Backward compatibility
+ homepage: web_url,
+ url: url_to_repo,
+ ssh_url: ssh_url_to_repo,
+ http_url: http_url_to_repo
}
end
@@ -769,6 +843,7 @@ class Project < ActiveRecord::Base
end
def change_head(branch)
+ repository.before_change_head
gitlab_shell.update_repository_head(self.path_with_namespace, branch)
reload_default_branch
end
@@ -825,6 +900,10 @@ class Project < ActiveRecord::Base
jira_tracker? && jira_service.active
end
+ def allowed_to_share_with_group?
+ !namespace.share_with_group_lock
+ end
+
def ci_commit(sha)
ci_commits.find_by(sha: sha)
end
@@ -856,13 +935,13 @@ class Project < ActiveRecord::Base
end
def valid_runners_token? token
- self.runners_token && self.runners_token == token
+ self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
# TODO (ayufan): For now we use runners_token (backward compatibility)
# In 8.4 every build will have its own individual token valid for time of build
def valid_build_token? token
- self.builds_enabled? && self.runners_token && self.runners_token == token
+ self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
def build_coverage_enabled?
@@ -885,4 +964,12 @@ class Project < ActiveRecord::Base
return true unless forked?
Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i)
end
+
+ def runners_token
+ ensure_runners_token!
+ end
+
+ def wiki
+ @wiki ||= ProjectWiki.new(self, self.owner)
+ end
end
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
new file mode 100644
index 00000000000..e52a6bd7c84
--- /dev/null
+++ b/app/models/project_group_link.rb
@@ -0,0 +1,36 @@
+class ProjectGroupLink < ActiveRecord::Base
+ GUEST = 10
+ REPORTER = 20
+ DEVELOPER = 30
+ MASTER = 40
+
+ belongs_to :project
+ belongs_to :group
+
+ validates :project_id, presence: true
+ validates :group_id, presence: true
+ validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
+ validates :group_access, presence: true
+ validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
+ validate :different_group
+
+ def self.access_options
+ Gitlab::Access.options
+ end
+
+ def self.default_access
+ DEVELOPER
+ end
+
+ def human_access
+ self.class.access_options.key(self.group_access)
+ end
+
+ private
+
+ def different_group
+ if self.group && self.project && self.project.group == self.group
+ errors.add(:base, "Project cannot be shared with the project it is in.")
+ end
+ end
+end
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index e6e16058d41..792ad804575 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -16,7 +16,9 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
+
require 'asana'
class AsanaService < Service
@@ -40,8 +42,8 @@ get the commit comment added to it.
You can also close a task with a message containing: `fix #123456`.
-You can find your Api Keys here:
-http://developer.asana.com/documentation/#api_keys'
+You can create a Personal Access Token here:
+http://app.asana.com/-/account_api'
end
def to_param
@@ -53,14 +55,12 @@ http://developer.asana.com/documentation/#api_keys'
{
type: 'text',
name: 'api_key',
- placeholder: 'User API token. User must have access to task,
-all comments will be attributed to this user.'
+ placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.'
},
{
type: 'text',
name: 'restrict_to_branch',
- placeholder: 'Comma-separated list of branches which will be
-automatically inspected. Leave blank to include all branches.'
+ placeholder: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
}
]
end
@@ -69,58 +69,58 @@ automatically inspected. Leave blank to include all branches.'
%w(push)
end
+ def client
+ @_client ||= begin
+ Asana::Client.new do |c|
+ c.authentication :access_token, api_key
+ end
+ end
+ end
+
def execute(data)
return unless supported_events.include?(data[:object_kind])
- Asana.configure do |client|
- client.api_key = api_key
- end
-
- user = data[:user_name]
+ # check the branch restriction is poplulated and branch is not included
branch = Gitlab::Git.ref_name(data[:ref])
-
branch_restriction = restrict_to_branch.to_s
-
- # check the branch restriction is poplulated and branch is not included
if branch_restriction.length > 0 && branch_restriction.index(branch).nil?
return
end
+ user = data[:user_name]
project_name = project.name_with_namespace
- push_msg = user + ' pushed to branch ' + branch + ' of ' + project_name
data[:commits].each do |commit|
- check_commit(' ( ' + commit[:url] + ' ): ' + commit[:message], push_msg)
+ push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):"
+ check_commit(commit[:message], push_msg)
end
end
def check_commit(message, push_msg)
- task_list = []
- close_list = []
-
- message.split("\n").each do |line|
- # look for a task ID or a full Asana url
- task_list.concat(line.scan(/#(\d+)/))
- task_list.concat(line.scan(/https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)/))
- # look for a word starting with 'fix' followed by a task ID
- close_list.concat(line.scan(/(fix\w*)\W*#(\d+)/i))
- end
-
- # post commit to every taskid found
- task_list.each do |taskid|
- task = Asana::Task.find(taskid[0])
-
- if task
- task.create_story(text: push_msg + ' ' + message)
- end
- end
-
- # close all tasks that had 'fix(ed/es/ing) #:id' in them
- close_list.each do |taskid|
- task = Asana::Task.find(taskid.last)
-
- if task
- task.modify(completed: true)
+ # matches either:
+ # - #1234
+ # - https://app.asana.com/0/0/1234
+ # optionally preceded with:
+ # - fix/ed/es/ing
+ # - close/s/d
+ # - closing
+ issue_finder = /(fix\w*|clos[ei]\w*+)?\W*(?:https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)|#(\d+))/i
+
+ message.scan(issue_finder).each do |tuple|
+ # tuple will be
+ # [ 'fix', 'id_from_url', 'id_from_pound' ]
+ taskid = tuple[2] || tuple[1]
+
+ begin
+ task = Asana::Task.find_by_id(client, taskid)
+ task.add_comment(text: "#{push_msg} #{message}")
+
+ if tuple[0]
+ task.update(completed: true)
+ end
+ rescue => e
+ Rails.logger.error(e.message)
+ next
end
end
end
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index fb7e0c0fb0d..29d841faed8 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class AssemblaService < Service
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index aa8746beb80..9e7f642180e 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class BambooService < CiService
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 199ee3a9d0d..3efbfd2eec3 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require "addressable/uri"
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index 8247c79fc33..f6313255cbb 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class BuildsEmailService < Service
@@ -72,12 +73,16 @@ class BuildsEmailService < Service
when 'success'
!notify_only_broken_builds?
when 'failed'
- true
+ !allow_failure?(data)
else
false
end
end
+ def allow_failure?(data)
+ data[:build_allow_failure] == true
+ end
+
def all_recipients(data)
all_recipients = recipients.split(',')
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index e591afdda64..6e8f0842524 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class CampfireService < Service
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index 88186113c68..d9f0849d147 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -16,20 +16,19 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
# Base class for CI services
# List methods you need to implement to get your CI service
# working with GitLab Merge Requests
class CiService < Service
- def category
- :ci
- end
-
+ default_value_for :category, 'ci'
+
def valid_token?(token)
- self.respond_to?(:token) && self.token.present? && self.token == token
+ self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
-
+
def supported_events
%w(push)
end
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index 7c2027c18e6..88a3e9218cb 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class CustomIssueTrackerService < IssueTrackerService
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 08e5ccb3855..b4724bb647e 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class DroneCiService < CiService
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index 8f5d8b086eb..b831577cd97 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class EmailsOnPushService < Service
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index 74c57949b4d..b402b68665a 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class ExternalWikiService < Service
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 15c7c907f7e..8605ce66e48 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require "flowdock-git-hook"
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 202fee042e3..61babe9cfe5 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require "gemnasium/gitlab_service"
diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb
index b64d97ce75d..33f0d7ea01a 100644
--- a/app/models/project_services/gitlab_ci_service.rb
+++ b/app/models/project_services/gitlab_ci_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
# TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 9558292fea3..05436cd0f79 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class GitlabIssueTrackerService < IssueTrackerService
@@ -23,9 +24,7 @@ class GitlabIssueTrackerService < IssueTrackerService
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
- def default?
- true
- end
+ default_value_for :default, true
def to_param
'gitlab'
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 1e1686a11c6..0e3fa4a40fe 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class HipchatService < Service
@@ -119,13 +120,13 @@ class HipchatService < Service
message << "#{push[:user_name]} "
if Gitlab::Git.blank_ref?(before)
message << "pushed new #{ref_type} <a href=\""\
- "#{project_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"\
+ "#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"\
" to #{project_link}\n"
elsif Gitlab::Git.blank_ref?(after)
message << "removed #{ref_type} <b>#{ref}</b> from <a href=\"#{project.web_url}\">#{project_name}</a> \n"
else
message << "pushed to #{ref_type} <a href=\""\
- "#{project.web_url}/commits/#{URI.escape(ref)}\">#{ref}</a> "
+ "#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> "
message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> "
message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)"
@@ -254,8 +255,8 @@ class HipchatService < Service
status = data[:commit][:status]
duration = data[:commit][:duration]
- branch_link = "<a href=\"#{project_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"
- commit_link = "<a href=\"#{project_url}/commit/#{URI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
+ branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"
+ commit_link = "<a href=\"#{project_url}/commit/#{CGI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
end
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index d24aa317cf3..04c714bfaad 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require 'uri'
@@ -72,9 +73,10 @@ class IrkerService < Service
'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\
'you want the channel to be a nickname instead, append ",isnick" to ' \
'the channel name; if the channel is protected by a secret password, ' \
- ' append "?key=secretpassword" to the URI. Note that if you specify a ' \
- ' default IRC URI to prepend before each recipient, you can just give ' \
- ' a channel name.' },
+ ' append "?key=secretpassword" to the URI (Note that due to a bug, if you ' \
+ ' want to use a password, you have to omit the "#" on the channel). If you ' \
+ ' specify a default IRC URI to prepend before each recipient, you can just ' \
+ ' give a channel name.' },
{ type: 'checkbox', name: 'colorize_messages' },
]
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 936e574cccd..25045224ce5 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -16,18 +16,17 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class IssueTrackerService < Service
validates :project_url, :issues_url, :new_issue_url, presence: true, if: :activated?
- def category
- :issue_tracker
- end
+ default_value_for :category, 'issue_tracker'
def default?
- false
+ default
end
def issue_url(iid)
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index e216f406e1c..aba37921c09 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class JiraService < IssueTrackerService
@@ -39,15 +40,10 @@ class JiraService < IssueTrackerService
end
def help
- line1 = 'Setting `project_url`, `issues_url` and `new_issue_url` will '\
+ 'Setting `project_url`, `issues_url` and `new_issue_url` will '\
'allow a user to easily navigate to the Jira issue tracker. See the '\
'[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\
'for details.'
-
- line2 = 'Support for referencing commits and automatic closing of Jira issues directly '\
- 'from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html)'
-
- [line1, line2].join("\n\n")
end
def title
@@ -112,7 +108,8 @@ class JiraService < IssueTrackerService
},
entity: {
name: noteable_name.humanize.downcase,
- url: entity_url
+ url: entity_url,
+ title: noteable.title
}
}
@@ -120,6 +117,7 @@ class JiraService < IssueTrackerService
end
def test_settings
+ return unless api_url.present?
result = JiraService.get(
jira_api_test_url,
headers: {
@@ -199,10 +197,11 @@ class JiraService < IssueTrackerService
user_url = data[:user][:url]
entity_name = data[:entity][:name]
entity_url = data[:entity][:url]
+ entity_title = data[:entity][:title]
project_name = data[:project][:name]
message = {
- body: "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]."
+ body: %Q{[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'}
}
unless existing_comment?(issue_name, message[:body])
@@ -217,6 +216,7 @@ class JiraService < IssueTrackerService
end
def send_message(url, message)
+ return unless api_url.present?
result = JiraService.post(
url,
body: message,
@@ -242,6 +242,7 @@ class JiraService < IssueTrackerService
end
def existing_comment?(issue_name, new_comment)
+ return unless api_url.present?
result = JiraService.get(
comment_url(issue_name),
headers: {
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index ade9ee97873..c9a890c7e3f 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class PivotaltrackerService < Service
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index 53edf522e9a..e76d9eca2ab 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class PushoverService < Service
@@ -111,7 +112,7 @@ class PushoverService < Service
priority: priority,
title: "#{project.name_with_namespace}",
message: message,
- url: data[:repository][:homepage],
+ url: data[:project][:web_url],
url_title: "See project #{project.name_with_namespace}"
}
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index dd9ba97ee1f..de974354c77 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class RedmineService < IssueTrackerService
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index 375b4534d07..d89cf6d17b2 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class SlackService < Service
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index a63700693d7..b8e9416131a 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class TeamcityService < CiService
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index 9e2c1b0e18e..1f7d85a5f3d 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
-# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
@@ -23,6 +22,4 @@ class ProjectSnippet < Snippet
# Scopes
scope :fresh, -> { order("created_at DESC") }
- scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
- scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 9f380a382cb..70a8bbaba65 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -136,7 +136,7 @@ class ProjectTeam
end
def human_max_access(user_id)
- Gitlab::Access.options.key max_member_access(user_id)
+ Gitlab::Access.options_with_owner.key(max_member_access(user_id))
end
# This method assumes project and group members are eager loaded for optimal
@@ -160,7 +160,27 @@ class ProjectTeam
end
end
- access.max
+ if project.invited_groups.any? && project.allowed_to_share_with_group?
+ access << max_invited_level(user_id)
+ end
+
+ access.compact.max
+ end
+
+
+ def max_invited_level(user_id)
+ project.project_group_links.map do |group_link|
+ invited_group = group_link.group
+ access = invited_group.group_members.find_by(user_id: user_id).try(:access_field)
+
+ # If group member has higher access level we should restrict it
+ # to max allowed access level
+ if access && access > group_link.group_access
+ access = group_link.group_access
+ end
+
+ access
+ end.compact.max
end
private
@@ -168,6 +188,35 @@ class ProjectTeam
def fetch_members(level = nil)
project_members = project.project_members
group_members = group ? group.group_members : []
+ invited_members = []
+
+ if project.invited_groups.any? && project.allowed_to_share_with_group?
+ project.project_group_links.each do |group_link|
+ invited_group = group_link.group
+ im = invited_group.group_members
+
+ if level
+ int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
+
+ # Skip group members if we ask for masters
+ # but max group access is developers
+ next if int_level > group_link.group_access
+
+ # If we ask for developers and max
+ # group access is developers we need to provide
+ # both group master, developers as devs
+ if int_level == group_link.group_access
+ im.where("access_level >= ?)", group_link.group_access)
+ else
+ im.send(level)
+ end
+ end
+
+ invited_members << im
+ end
+
+ invited_members = invited_members.flatten.compact
+ end
if level
project_members = project_members.send(level)
@@ -175,6 +224,7 @@ class ProjectTeam
end
user_ids = project_members.pluck(:user_id)
+ user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
user_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids)
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index b5fec38378b..59b1b86d1fb 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -2,7 +2,7 @@ class ProjectWiki
include Gitlab::ShellAdapter
MARKUPS = {
- 'Markdown' => :md,
+ 'Markdown' => :markdown,
'RDoc' => :rdoc,
'AsciiDoc' => :asciidoc
} unless defined?(MARKUPS)
@@ -12,6 +12,7 @@ class ProjectWiki
# Returns a string describing what went wrong after
# an operation fails.
attr_reader :error_message
+ attr_reader :project
def initialize(project, user = nil)
@project = project
@@ -38,11 +39,15 @@ class ProjectWiki
[Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
end
+ def wiki_base_path
+ ["/", @project.path_with_namespace, "/wikis"].join('')
+ end
+
# Returns the Gollum::Wiki object.
def wiki
@wiki ||= begin
Gollum::Wiki.new(path_to_repo)
- rescue Gollum::NoSuchPathError
+ rescue Rugged::OSError
create_repo!
end
end
@@ -85,7 +90,7 @@ class ProjectWiki
def create_page(title, content, format = :markdown, message = nil)
commit = commit_details(:created, message, title)
- wiki.write_page(title, format, content, commit)
+ wiki.write_page(title, format.to_sym, content, commit)
update_project_activity
rescue Gollum::DuplicatePageError => e
@@ -96,7 +101,7 @@ class ProjectWiki
def update_page(page, content, format = :markdown, message = nil)
commit = commit_details(:updated, message, page.title)
- wiki.update_page(page, page.name, format, content, commit)
+ wiki.update_page(page, page.name, format.to_sym, content, commit)
update_project_activity
end
@@ -118,7 +123,7 @@ class ProjectWiki
end
def repository
- Repository.new(path_with_namespace, default_branch, @project)
+ Repository.new(path_with_namespace, @project)
end
def default_branch
diff --git a/app/models/repository.rb b/app/models/repository.rb
index a9bf4eb4033..25d24493f6e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -3,6 +3,10 @@ require 'securerandom'
class Repository
class CommitError < StandardError; end
+ # Files to use as a project avatar in case no avatar was uploaded via the web
+ # UI.
+ AVATAR_FILES = %w{logo.png logo.jpg logo.gif}
+
include Gitlab::ShellAdapter
attr_accessor :path_with_namespace, :project
@@ -15,7 +19,7 @@ class Repository
Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
end
- def initialize(path_with_namespace, default_branch = nil, project = nil)
+ def initialize(path_with_namespace, project)
@path_with_namespace = path_with_namespace
@project = project
end
@@ -23,13 +27,11 @@ class Repository
def raw_repository
return nil unless path_with_namespace
- @raw_repository ||= begin
- repo = Gitlab::Git::Repository.new(path_to_repo)
- repo.autocrlf = :input
- repo
- rescue Gitlab::Git::Repository::NoRepository
- nil
- end
+ @raw_repository ||= Gitlab::Git::Repository.new(path_to_repo)
+ end
+
+ def update_autocrlf_option
+ raw_repository.autocrlf = :input if raw_repository.autocrlf != :input
end
# Return absolute path to repository
@@ -40,11 +42,18 @@ class Repository
end
def exists?
- raw_repository
+ return false unless raw_repository
+
+ raw_repository.rugged
+ true
+ rescue Gitlab::Git::Repository::NoRepository
+ false
end
def empty?
- raw_repository.empty?
+ return @empty unless @empty.nil?
+
+ @empty = cache.fetch(:empty?) { raw_repository.empty? }
end
#
@@ -57,11 +66,15 @@ class Repository
# This method return true if repository contains some content visible in project page.
#
def has_visible_content?
- !raw_repository.branches.empty?
+ return @has_visible_content unless @has_visible_content.nil?
+
+ @has_visible_content = cache.fetch(:has_visible_content?) do
+ raw_repository.branch_count > 0
+ end
end
def commit(id = 'HEAD')
- return nil unless raw_repository
+ return nil unless exists?
commit = Gitlab::Git::Commit.find(raw_repository, id)
commit = Commit.new(commit, @project) if commit
commit
@@ -78,7 +91,8 @@ class Repository
offset: offset,
# --follow doesn't play well with --skip. See:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
- follow: false
+ follow: false,
+ skip_merges: skip_merges
}
commits = Gitlab::Git::Commit.where(options)
@@ -92,9 +106,12 @@ class Repository
commits
end
- def find_commits_by_message(query)
+ def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
+ ref ||= root_ref
+
# Limited to 1000 commits for now, could be parameterized?
- args = %W(#{Gitlab.config.git.bin_path} log --pretty=%H --max-count 1000 --grep=#{query})
+ args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query})
+ args = args.concat(%W(-- #{path})) if path.present?
git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp)
commits = git_log_results.map { |c| commit(c) }
@@ -120,18 +137,18 @@ class Repository
rugged.branches.create(branch_name, target)
end
- expire_branches_cache
+ after_create_branch
find_branch(branch_name)
end
def add_tag(tag_name, ref, message = nil)
- expire_tags_cache
+ before_push_tag
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end
def rm_branch(user, branch_name)
- expire_branches_cache
+ before_remove_branch
branch = find_branch(branch_name)
oldrev = branch.try(:target)
@@ -142,12 +159,12 @@ class Repository
rugged.branches.delete(branch_name)
end
- expire_branches_cache
+ after_remove_branch
true
end
def rm_tag(tag_name)
- expire_tags_cache
+ before_remove_tag
gitlab_shell.rm_tag(path_with_namespace, tag_name)
end
@@ -170,12 +187,35 @@ class Repository
end
end
+ def branch_count
+ @branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count }
+ end
+
+ def tag_count
+ @tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count }
+ end
+
# Return repo size in megabytes
# Cached in redis
def size
cache.fetch(:size) { raw_repository.size }
end
+ def diverging_commit_counts(branch)
+ root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
+ cache.fetch(:"diverging_commit_counts_#{branch.name}") do
+ # Rugged seems to throw a `ReferenceError` when given branch_names rather
+ # than SHA-1 hashes
+ number_commits_behind = raw_repository.
+ count_commits_between(branch.target, root_ref_hash)
+
+ number_commits_ahead = raw_repository.
+ count_commits_between(root_ref_hash, branch.target)
+
+ { behind: number_commits_behind, ahead: number_commits_ahead }
+ end
+ end
+
def cache_keys
%i(size branch_names tag_names commit_count
readme version contribution_guide changelog license)
@@ -199,19 +239,62 @@ class Repository
@branches = nil
end
- def expire_cache
+ def expire_cache(branch_name = nil, revision = nil)
cache_keys.each do |key|
cache.expire(key)
end
+
+ expire_branch_cache(branch_name)
+ expire_avatar_cache(branch_name, revision)
+
+ # This ensures this particular cache is flushed after the first commit to a
+ # new repository.
+ expire_emptiness_caches if empty?
end
- def rebuild_cache
- cache_keys.each do |key|
- cache.expire(key)
- send(key)
+ def expire_branch_cache(branch_name = nil)
+ # When we push to the root branch we have to flush the cache for all other
+ # branches as their statistics are based on the commits relative to the
+ # root branch.
+ if !branch_name || branch_name == root_ref
+ branches.each do |branch|
+ cache.expire(:"diverging_commit_counts_#{branch.name}")
+ end
+ # In case a commit is pushed to a non-root branch we only have to flush the
+ # cache for said branch.
+ else
+ cache.expire(:"diverging_commit_counts_#{branch_name}")
end
end
+ def expire_root_ref_cache
+ cache.expire(:root_ref)
+ @root_ref = nil
+ end
+
+ # Expires the cache(s) used to determine if a repository is empty or not.
+ def expire_emptiness_caches
+ cache.expire(:empty?)
+ @empty = nil
+
+ expire_has_visible_content_cache
+ end
+
+ def expire_has_visible_content_cache
+ cache.expire(:has_visible_content?)
+ @has_visible_content = nil
+ end
+
+ def expire_branch_count_cache
+ cache.expire(:branch_count)
+ @branch_count = nil
+ end
+
+ def expire_tag_count_cache
+ cache.expire(:tag_count)
+ @tag_count = nil
+ end
+
def lookup_cache
@lookup_cache ||= {}
end
@@ -220,6 +303,80 @@ class Repository
cache.expire(:branch_names)
end
+ def expire_avatar_cache(branch_name = nil, revision = nil)
+ # Avatars are pulled from the default branch, thus if somebody pushes to a
+ # different branch there's no need to expire anything.
+ return if branch_name && branch_name != root_ref
+
+ # We don't want to flush the cache if the commit didn't actually make any
+ # changes to any of the possible avatar files.
+ if revision && commit = self.commit(revision)
+ return unless commit.diffs.
+ any? { |diff| AVATAR_FILES.include?(diff.new_path) }
+ end
+
+ cache.expire(:avatar)
+
+ @avatar = nil
+ end
+
+ # Runs code just before a repository is deleted.
+ def before_delete
+ expire_cache if exists?
+
+ expire_root_ref_cache
+ expire_emptiness_caches
+ end
+
+ # Runs code just before the HEAD of a repository is changed.
+ def before_change_head
+ # Cached divergent commit counts are based on repository head
+ expire_branch_cache
+ expire_root_ref_cache
+ end
+
+ # Runs code before pushing (= creating or removing) a tag.
+ def before_push_tag
+ expire_cache
+ expire_tags_cache
+ expire_tag_count_cache
+ end
+
+ # Runs code before removing a tag.
+ def before_remove_tag
+ expire_tags_cache
+ expire_tag_count_cache
+ end
+
+ # Runs code after a repository has been forked/imported.
+ def after_import
+ expire_emptiness_caches
+ end
+
+ # Runs code after a new commit has been pushed.
+ def after_push_commit(branch_name, revision)
+ expire_cache(branch_name, revision)
+ end
+
+ # Runs code after a new branch has been created.
+ def after_create_branch
+ expire_branches_cache
+ expire_has_visible_content_cache
+ expire_branch_count_cache
+ end
+
+ # Runs code before removing an existing branch.
+ def before_remove_branch
+ expire_branches_cache
+ end
+
+ # Runs code after an existing branch has been removed.
+ def after_remove_branch
+ expire_has_visible_content_cache
+ expire_branch_count_cache
+ expire_branches_cache
+ end
+
def method_missing(m, *args, &block)
if m == :lookup && !block_given?
lookup_cache[m] ||= {}
@@ -437,7 +594,7 @@ class Repository
end
def root_ref
- @root_ref ||= raw_repository.root_ref
+ @root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
end
def commit_dir(user, path, message, branch)
@@ -536,6 +693,42 @@ class Repository
end
end
+ def revert(user, commit, base_branch, revert_tree_id = nil)
+ source_sha = find_branch(base_branch).target
+ revert_tree_id ||= check_revert_content(commit, base_branch)
+
+ return false unless revert_tree_id
+
+ commit_with_hooks(user, base_branch) do |ref|
+ committer = user_to_committer(user)
+ source_sha = Rugged::Commit.create(rugged,
+ message: commit.revert_message,
+ author: committer,
+ committer: committer,
+ tree: revert_tree_id,
+ parents: [rugged.lookup(source_sha)],
+ update_ref: ref)
+ end
+ end
+
+ def check_revert_content(commit, base_branch)
+ source_sha = find_branch(base_branch).target
+ args = [commit.id, source_sha]
+ args << { mainline: 1 } if commit.merge_commit?
+
+ revert_index = rugged.revert_commit(*args)
+ return false if revert_index.conflicts?
+
+ tree_id = revert_index.write_tree(rugged)
+ return false unless diff_exists?(source_sha, tree_id)
+
+ tree_id
+ end
+
+ def diff_exists?(sha1, sha2)
+ rugged.diff(sha1, sha2).size > 0
+ end
+
def merged_to_root_ref?(branch_name)
branch_commit = commit(branch_name)
root_ref_commit = commit(root_ref)
@@ -548,7 +741,11 @@ class Repository
end
def merge_base(first_commit_id, second_commit_id)
+ first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
+ second_commit_id = commit(second_commit_id).try(:id) || second_commit_id
rugged.merge_base(first_commit_id, second_commit_id)
+ rescue Rugged::ReferenceError
+ nil
end
def is_ancestor?(ancestor_id, descendant_id)
@@ -558,19 +755,22 @@ class Repository
def search_files(query, ref)
offset = 2
- args = %W(#{Gitlab.config.git.bin_path} grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref})
+ args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
def parse_search_result(result)
ref = nil
filename = nil
+ basename = nil
startline = 0
result.each_line.each_with_index do |line, index|
if line =~ /^.*:.*:\d+:/
ref, filename, startline = line.split(':')
startline = startline.to_i - index
+ extname = File.extname(filename)
+ basename = filename.sub(/#{extname}$/, '')
break
end
end
@@ -583,6 +783,7 @@ class Repository
OpenStruct.new(
filename: filename,
+ basename: basename,
ref: ref,
startline: startline,
data: data
@@ -609,12 +810,15 @@ class Repository
end
def commit_with_hooks(current_user, branch)
+ update_autocrlf_option
+
oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
+ target_branch = find_branch(branch)
was_empty = empty?
- unless was_empty
- oldrev = find_branch(branch).target
+ if !was_empty && target_branch
+ oldrev = target_branch.target
end
with_tmp_ref(oldrev) do |tmp_ref|
@@ -626,7 +830,7 @@ class Repository
end
GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
- if was_empty
+ if was_empty || !target_branch
# Create branch
rugged.references.create(ref, newrev)
else
@@ -641,6 +845,27 @@ class Repository
end
end
end
+
+ newrev
+ end
+ end
+
+ def ls_files(ref)
+ actual_ref = ref || root_ref
+ raw_repository.ls_files(actual_ref)
+ end
+
+ def main_language
+ unless empty?
+ Linguist::Repository.new(rugged, rugged.head.target_id).language
+ end
+ end
+
+ def avatar
+ @avatar ||= cache.fetch(:avatar) do
+ AVATAR_FILES.find do |file|
+ blob_at_branch('master', file)
+ end
end
end
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index f36eda1531b..77115597d71 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -25,8 +25,6 @@ class SentNotification < ActiveRecord::Base
class << self
def reply_key
- return nil unless Gitlab::IncomingEmail.enabled?
-
SecureRandom.hex(16)
end
@@ -59,11 +57,15 @@ class SentNotification < ActiveRecord::Base
def record_note(note, recipient_id, reply_key, params = {})
params[:line_code] = note.line_code
-
+
record(note.noteable, recipient_id, reply_key, params)
end
end
+ def unsubscribable?
+ !for_commit?
+ end
+
def for_commit?
noteable_type == "Commit"
end
@@ -75,4 +77,8 @@ class SentNotification < ActiveRecord::Base
super
end
end
+
+ def to_param
+ self.reply_key
+ end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index d3bf7f0ebd1..721273250ea 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
# To add new service you should build a class inherited from Service
@@ -42,6 +43,9 @@ class Service < ActiveRecord::Base
validates :project_id, presence: true, unless: Proc.new { |service| service.template? }
scope :visible, -> { where.not(type: ['GitlabIssueTrackerService', 'GitlabCiService']) }
+ scope :issue_trackers, -> { where(category: 'issue_tracker') }
+ scope :active, -> { where(active: true) }
+ scope :without_defaults, -> { where(default: false) }
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
@@ -50,6 +54,8 @@ class Service < ActiveRecord::Base
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
+ default_value_for :category, 'common'
+
def activated?
active
end
@@ -59,7 +65,7 @@ class Service < ActiveRecord::Base
end
def category
- :common
+ read_attribute(:category).to_sym
end
def initialize_properties
@@ -152,7 +158,7 @@ class Service < ActiveRecord::Base
# Returns a hash of the properties that have been assigned a new value since last save,
# indicating their original values (attr => original value).
- # ActiveRecord does not provide a mechanism to track changes in serialized keys,
+ # ActiveRecord does not provide a mechanism to track changes in serialized keys,
# so we need a specific implementation for service properties.
# This allows to track changes to properties set with the accessor methods,
# but not direct manipulation of properties hash.
@@ -163,7 +169,7 @@ class Service < ActiveRecord::Base
def reset_updated_properties
@updated_properties = nil
end
-
+
def async_execute(data)
return unless supported_events.include?(data[:object_kind])
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index f876be7a4c8..b9e835a4486 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
-# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
@@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") }
- 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
@@ -111,21 +108,37 @@ class Snippet < ActiveRecord::Base
nil
end
- def expired?
- expires_at && expires_at < Time.current
- end
-
def visibility_level_field
visibility_level
end
class << self
+ # Searches for snippets with a matching title or file name.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String.
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
end
+ # Searches for snippets with matching content.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String.
+ #
+ # Returns an ActiveRecord::Relation.
def search_code(query)
- where('(content LIKE :query)', query: "%#{query}%")
+ table = Snippet.arel_table
+ pattern = "%#{query}%"
+
+ where(table[:content].matches(pattern))
end
def accessible_to(user)
diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb
new file mode 100644
index 00000000000..12df68ef83b
--- /dev/null
+++ b/app/models/spam_log.rb
@@ -0,0 +1,10 @@
+class SpamLog < ActiveRecord::Base
+ belongs_to :user
+
+ validates :user, presence: true
+
+ def remove_user
+ user.block
+ user.destroy
+ end
+end
diff --git a/app/models/spam_report.rb b/app/models/spam_report.rb
new file mode 100644
index 00000000000..cdc7321b08e
--- /dev/null
+++ b/app/models/spam_report.rb
@@ -0,0 +1,5 @@
+class SpamReport < ActiveRecord::Base
+ belongs_to :user
+
+ validates :user, presence: true
+end
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index dd75d3ab8ba..dd800ce110f 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -15,7 +15,7 @@ class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :subscribable, polymorphic: true
- validates :user_id,
+ validates :user_id,
uniqueness: { scope: [:subscribable_id, :subscribable_type] },
presence: true
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
new file mode 100644
index 00000000000..5f91991f781
--- /dev/null
+++ b/app/models/todo.rb
@@ -0,0 +1,53 @@
+# == Schema Information
+#
+# Table name: todos
+#
+# id :integer not null, primary key
+# user_id :integer not null
+# project_id :integer not null
+# target_id :integer not null
+# target_type :string not null
+# author_id :integer
+# note_id :integer
+# action :integer not null
+# state :string not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+class Todo < ActiveRecord::Base
+ ASSIGNED = 1
+ MENTIONED = 2
+
+ belongs_to :author, class_name: "User"
+ belongs_to :note
+ belongs_to :project
+ belongs_to :target, polymorphic: true, touch: true
+ belongs_to :user
+
+ delegate :name, :email, to: :author, prefix: true, allow_nil: true
+
+ validates :action, :project, :target, :user, presence: true
+
+ default_scope { reorder(id: :desc) }
+
+ scope :pending, -> { with_state(:pending) }
+ scope :done, -> { with_state(:done) }
+
+ state_machine :state, initial: :pending do
+ event :done do
+ transition [:pending, :done] => :done
+ end
+
+ state :pending
+ state :done
+ end
+
+ def body
+ if note.present?
+ note.note
+ else
+ target.title
+ end
+ end
+end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 93b3246a668..7c4ed6e393b 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -19,20 +19,28 @@ class Tree
available_readmes = blobs.select(&:readme?)
- if available_readmes.count == 0
- return @readme = nil
+ previewable_readmes = available_readmes.select do |blob|
+ previewable?(blob.name)
+ end
+
+ plain_readmes = available_readmes.select do |blob|
+ plain?(blob.name)
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
+ # Prioritize previewable over plain readmes
+ readme_tree = previewable_readmes.first || plain_readmes.first
+
+ # Return if we can't preview any of them
+ if readme_tree.nil?
+ return @readme = nil
+ end
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)
+ @readme.load_all_data!(git_repo)
+ @readme
end
def trees
diff --git a/app/models/user.rb b/app/models/user.rb
index df87f3b79bd..c011af03591 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,62 +2,64 @@
#
# Table name: users
#
-# id :integer not null, primary key
-# email :string(255) default(""), not null
-# encrypted_password :string(255) default(""), not null
-# reset_password_token :string(255)
-# reset_password_sent_at :datetime
-# remember_created_at :datetime
-# sign_in_count :integer default(0)
-# current_sign_in_at :datetime
-# last_sign_in_at :datetime
-# current_sign_in_ip :string(255)
-# last_sign_in_ip :string(255)
-# created_at :datetime
-# updated_at :datetime
-# name :string(255)
-# admin :boolean default(FALSE), not null
-# projects_limit :integer default(10)
-# skype :string(255) default(""), not null
-# linkedin :string(255) default(""), not null
-# twitter :string(255) default(""), not null
-# authentication_token :string(255)
-# theme_id :integer default(1), not null
-# bio :string(255)
-# failed_attempts :integer default(0)
-# locked_at :datetime
-# unlock_token :string(255)
-# username :string(255)
-# can_create_group :boolean default(TRUE), not null
-# can_create_team :boolean default(TRUE), not null
-# state :string(255)
-# color_scheme_id :integer default(1), not null
-# notification_level :integer default(1), not null
-# password_expires_at :datetime
-# created_by_id :integer
-# last_credential_check_at :datetime
-# avatar :string(255)
-# confirmation_token :string(255)
-# confirmed_at :datetime
-# confirmation_sent_at :datetime
-# unconfirmed_email :string(255)
-# hide_no_ssh_key :boolean default(FALSE)
-# website_url :string(255) default(""), not null
-# notification_email :string(255)
-# hide_no_password :boolean default(FALSE)
-# password_automatically_set :boolean default(FALSE)
-# location :string(255)
-# encrypted_otp_secret :string(255)
-# encrypted_otp_secret_iv :string(255)
-# encrypted_otp_secret_salt :string(255)
-# otp_required_for_login :boolean default(FALSE), not null
-# otp_backup_codes :text
-# public_email :string(255) default(""), not null
-# dashboard :integer default(0)
-# project_view :integer default(0)
-# consumed_timestep :integer
-# layout :integer default(0)
-# hide_project_limit :boolean default(FALSE)
+# id :integer not null, primary key
+# email :string(255) default(""), not null
+# encrypted_password :string(255) default(""), not null
+# reset_password_token :string(255)
+# reset_password_sent_at :datetime
+# remember_created_at :datetime
+# sign_in_count :integer default(0)
+# current_sign_in_at :datetime
+# last_sign_in_at :datetime
+# current_sign_in_ip :string(255)
+# last_sign_in_ip :string(255)
+# created_at :datetime
+# updated_at :datetime
+# name :string(255)
+# admin :boolean default(FALSE), not null
+# projects_limit :integer default(10)
+# skype :string(255) default(""), not null
+# linkedin :string(255) default(""), not null
+# twitter :string(255) default(""), not null
+# authentication_token :string(255)
+# theme_id :integer default(1), not null
+# bio :string(255)
+# failed_attempts :integer default(0)
+# locked_at :datetime
+# username :string(255)
+# can_create_group :boolean default(TRUE), not null
+# can_create_team :boolean default(TRUE), not null
+# state :string(255)
+# color_scheme_id :integer default(1), not null
+# notification_level :integer default(1), not null
+# password_expires_at :datetime
+# created_by_id :integer
+# last_credential_check_at :datetime
+# avatar :string(255)
+# confirmation_token :string(255)
+# confirmed_at :datetime
+# confirmation_sent_at :datetime
+# unconfirmed_email :string(255)
+# hide_no_ssh_key :boolean default(FALSE)
+# website_url :string(255) default(""), not null
+# notification_email :string(255)
+# hide_no_password :boolean default(FALSE)
+# password_automatically_set :boolean default(FALSE)
+# location :string(255)
+# encrypted_otp_secret :string(255)
+# encrypted_otp_secret_iv :string(255)
+# encrypted_otp_secret_salt :string(255)
+# otp_required_for_login :boolean default(FALSE), not null
+# otp_backup_codes :text
+# public_email :string(255) default(""), not null
+# dashboard :integer default(0)
+# project_view :integer default(0)
+# consumed_timestep :integer
+# layout :integer default(0)
+# hide_project_limit :boolean default(FALSE)
+# unlock_token :string
+# otp_grace_period_started_at :datetime
+# external :boolean default(FALSE)
#
require 'carrierwave/orm/activerecord'
@@ -76,6 +78,7 @@ class User < ActiveRecord::Base
add_authentication_token_field :authentication_token
default_value_for :admin, false
+ default_value_for :external, false
default_value_for :can_create_group, gitlab_config.default_can_create_group
default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false
@@ -137,18 +140,16 @@ class User < ActiveRecord::Base
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
has_one :abuse_report, dependent: :destroy
+ has_many :spam_logs, dependent: :destroy
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
-
+ has_many :todos, dependent: :destroy
#
# Validations
#
validates :name, presence: true
- # Note that a 'uniqueness' and presence check is provided by devise :validatable for email. We do not need to
- # duplicate that here as the validation framework will have duplicate errors in the event of a failure.
- validates :email, presence: true, email: { strict_mode: 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 :notification_email, presence: true, email: true
+ validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
validates :bio, length: { maximum: 255 }, allow_blank: true
validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :username,
@@ -172,6 +173,7 @@ class User < ActiveRecord::Base
after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
before_save :ensure_authentication_token
+ before_save :ensure_external_user_rights
after_save :ensure_namespace_correct
after_initialize :set_projects_limit
after_create :post_create_hook
@@ -195,10 +197,22 @@ class User < ActiveRecord::Base
state_machine :state, initial: :active do
event :block do
transition active: :blocked
+ transition ldap_blocked: :blocked
+ end
+
+ event :ldap_block do
+ transition active: :ldap_blocked
end
event :activate do
transition blocked: :active
+ transition ldap_blocked: :active
+ end
+
+ state :blocked, :ldap_blocked do
+ def blocked?
+ true
+ end
end
end
@@ -206,7 +220,8 @@ class User < ActiveRecord::Base
# Scopes
scope :admins, -> { where(admin: true) }
- scope :blocked, -> { with_state(:blocked) }
+ scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
+ scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
@@ -262,13 +277,29 @@ class User < ActiveRecord::Base
self.with_two_factor
when 'wop'
self.without_projects
+ when 'external'
+ self.external
else
self.active
end
end
+ # Searches users matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%")
+ table = arel_table
+ pattern = "%#{query}%"
+
+ where(
+ table[:name].matches(pattern).
+ or(table[:email].matches(pattern)).
+ or(table[:username].matches(pattern))
+ )
end
def by_login(login)
@@ -343,19 +374,24 @@ class User < ActiveRecord::Base
def disable_two_factor!
update_attributes(
- two_factor_enabled: false,
- encrypted_otp_secret: nil,
- encrypted_otp_secret_iv: nil,
- encrypted_otp_secret_salt: nil,
- otp_backup_codes: nil
+ two_factor_enabled: false,
+ encrypted_otp_secret: nil,
+ encrypted_otp_secret_iv: nil,
+ encrypted_otp_secret_salt: nil,
+ otp_grace_period_started_at: nil,
+ otp_backup_codes: nil
)
end
def namespace_uniq
+ # Return early if username already failed the first uniqueness validation
+ return if self.errors.key?(:username) &&
+ self.errors[:username].include?('has already been taken')
+
namespace_name = self.username
existing_namespace = Namespace.by_path(namespace_name)
if existing_namespace && existing_namespace != self.namespace
- self.errors.add :username, "already exists"
+ self.errors.add(:username, 'has already been taken')
end
end
@@ -588,6 +624,13 @@ class User < ActiveRecord::Base
end
end
+ def try_obtain_ldap_lease
+ # After obtaining this lease LDAP checks will be blocked for 600 seconds
+ # (10 minutes) for this user.
+ lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600)
+ lease.try_obtain
+ end
+
def solo_owned_groups
@solo_owned_groups ||= owned_groups.select do |group|
group.owners == [self]
@@ -648,7 +691,10 @@ class User < ActiveRecord::Base
end
def all_emails
- [self.email, *self.emails.map(&:email)]
+ all_emails = []
+ all_emails << self.email unless self.temp_oauth_email?
+ all_emails.concat(self.emails.map(&:email))
+ all_emails
end
def hook_attrs
@@ -784,7 +830,8 @@ class User < ActiveRecord::Base
def projects_union
Gitlab::SQL::Union.new([personal_projects.select(:id),
groups_projects.select(:id),
- projects.select(:id)])
+ projects.select(:id),
+ groups.joins(:shared_projects).select(:project_id)])
end
def ci_projects_union
@@ -800,4 +847,11 @@ class User < ActiveRecord::Base
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
end
+
+ def ensure_external_user_rights
+ return unless self.external?
+
+ self.can_create_group = false
+ self.projects_limit = 0
+ end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index e9413c34bae..526760779a4 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -62,7 +62,7 @@ class WikiPage
# The raw content of this page.
def content
@attributes[:content] ||= if @page
- @page.raw_data
+ @page.text_data
end
end
@@ -110,7 +110,7 @@ class WikiPage
# Returns boolean True or False if this instance
# is an old version of the page.
def historical?
- @page.historical?
+ @page.historical? && versions.first.sha != version.sha
end
# Returns boolean True or False if this instance
@@ -169,7 +169,7 @@ class WikiPage
private
def set_attributes
- attributes[:slug] = @page.escaped_url_path
+ attributes[:slug] = @page.url_path
attributes[:title] = @page.title
attributes[:format] = @page.format
end
diff --git a/app/services/archive_repository_service.rb b/app/services/archive_repository_service.rb
deleted file mode 100644
index 2160bf13e6d..00000000000
--- a/app/services/archive_repository_service.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class ArchiveRepositoryService
- attr_reader :project, :ref, :format
-
- def initialize(project, ref, format)
- format ||= 'tar.gz'
- @project, @ref, @format = project, ref, format.downcase
- end
-
- def execute(options = {})
- RepositoryArchiveCacheWorker.perform_async
-
- metadata = project.repository.archive_metadata(ref, storage_path, format)
- raise "Repository or ref not found" if metadata.empty?
-
- metadata
- end
-
- private
-
- def storage_path
- Gitlab.config.gitlab.repository_downloads_path
- end
-end
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index b48ca67d4d2..8563633816c 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -23,6 +23,10 @@ class BaseService
EventCreateService.new
end
+ def todo_service
+ TodoService.new
+ end
+
def log_info(message)
Gitlab::AppLogger.info message
end
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
index ad901f2da5d..002f7ba1278 100644
--- a/app/services/ci/create_builds_service.rb
+++ b/app/services/ci/create_builds_service.rb
@@ -34,6 +34,7 @@ module Ci
build = commit.builds.create!(build_attrs)
build.execute_hooks
+ build
end
end
end
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index f469b13e902..50c95ced8a7 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -1,28 +1,23 @@
module Ci
class ImageForBuildService
- def execute(project, params)
- sha = params[:sha]
- sha ||=
- if params[:ref]
- project.commit(params[:ref]).try(:sha)
- end
+ def execute(project, opts)
+ sha = opts[:sha] || ref_sha(project, opts[:ref])
- commit = project.ci_commits.ordered.find_by(sha: sha)
+ commit = project.ci_commits.find_by(sha: sha)
image_name = image_for_commit(commit)
image_path = Rails.root.join('public/ci', image_name)
-
- OpenStruct.new(
- path: image_path,
- name: image_name
- )
+ OpenStruct.new(path: image_path, name: image_name)
end
private
+ def ref_sha(project, ref)
+ project.commit(ref).try(:sha) if ref
+ end
+
def image_for_commit(commit)
return 'build-unknown.svg' unless commit
-
'build-' + commit.status + ".svg"
end
end
diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb
new file mode 100644
index 00000000000..a3c950ede1f
--- /dev/null
+++ b/app/services/commits/revert_service.rb
@@ -0,0 +1,59 @@
+module Commits
+ class RevertService < ::BaseService
+ class ValidationError < StandardError; end
+ class ReversionError < StandardError; end
+
+ def execute
+ @source_project = params[:source_project] || @project
+ @target_branch = params[:target_branch]
+ @commit = params[:commit]
+ @create_merge_request = params[:create_merge_request].present?
+
+ check_push_permissions unless @create_merge_request
+ commit
+ rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
+ ValidationError, ReversionError => ex
+ error(ex.message)
+ end
+
+ def commit
+ revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch
+ revert_tree_id = repository.check_revert_content(@commit, @target_branch)
+
+ if revert_tree_id
+ create_target_branch(revert_into) if @create_merge_request
+
+ repository.revert(current_user, @commit, revert_into, revert_tree_id)
+ success
+ else
+ error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically.
+ It may have already been reverted, or a more recent commit may have updated some of its content."
+ raise ReversionError, error_msg
+ end
+ end
+
+ private
+
+ def create_target_branch(new_branch)
+ # Temporary branch exists and contains the revert commit
+ return success if repository.find_branch(new_branch)
+
+ result = CreateBranchService.new(@project, current_user)
+ .execute(new_branch, @target_branch, source_project: @source_project)
+
+ if result[:status] == :error
+ raise ReversionError, "There was an error creating the source branch: #{result[:message]}"
+ end
+ end
+
+ def check_push_permissions
+ allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
+
+ unless allowed
+ raise ValidationError.new('You are not allowed to push into this branch')
+ end
+
+ true
+ end
+ end
+end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index ec581658fc1..e2bccbdbcc3 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -1,7 +1,7 @@
require 'securerandom'
# Compare 2 branches for one repo or between repositories
-# and return Gitlab::CompareResult object that responds to commits and diffs
+# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
def execute(source_project, source_branch, target_project, target_branch, diff_options = {})
source_commit = source_project.commit(source_branch)
@@ -20,12 +20,10 @@ class CompareService
)
end
- Gitlab::CompareResult.new(
- Gitlab::Git::Compare.new(
- target_project.repository.raw_repository,
- target_branch,
- source_sha,
- ), diff_options
+ Gitlab::Git::Compare.new(
+ target_project.repository.raw_repository,
+ target_branch,
+ source_sha,
)
end
end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index f139872c728..707c2f7ff85 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -29,12 +29,7 @@ class CreateBranchService < BaseService
end
if new_branch
- push_data = build_push_data(project, current_user, new_branch)
-
- EventCreateService.new.push(project, current_user, push_data)
- project.execute_hooks(push_data.dup, :push_hooks)
- project.execute_services(push_data.dup, :push_hooks)
-
+ # GitPushService handles execution of services and hooks for branch pushes
success(new_branch)
else
error('Invalid reference name')
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
index 31b407efeb1..69d5c42a877 100644
--- a/app/services/create_commit_builds_service.rb
+++ b/app/services/create_commit_builds_service.rb
@@ -33,7 +33,6 @@ class CreateCommitBuildsService
unless commit.skip_ci?
# Create builds for commit
tag = Gitlab::Git.tag_ref?(origin_ref)
- commit.update_committed!
commit.create_builds(ref, tag, user)
end
diff --git a/app/services/create_spam_log_service.rb b/app/services/create_spam_log_service.rb
new file mode 100644
index 00000000000..59a66fde47a
--- /dev/null
+++ b/app/services/create_spam_log_service.rb
@@ -0,0 +1,13 @@
+class CreateSpamLogService < BaseService
+ def initialize(project, user, params)
+ super(project, user, params)
+ end
+
+ def execute
+ spam_params = params.merge({ user_id: @current_user.id,
+ project_id: @project.id } )
+ spam_log = SpamLog.new(spam_params)
+ spam_log.save
+ spam_log
+ end
+end
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
index 2452999382a..55985380d31 100644
--- a/app/services/create_tag_service.rb
+++ b/app/services/create_tag_service.rb
@@ -23,6 +23,7 @@ class CreateTagService < BaseService
EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :tag_push_hooks)
project.execute_services(push_data.dup, :tag_push_hooks)
+ CreateCommitBuildsService.new.execute(project, current_user, push_data)
if release_description
CreateReleaseService.new(@project, @current_user).
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 22bf9dd935e..fae069ee4a5 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -25,12 +25,7 @@ class DeleteBranchService < BaseService
end
if repository.rm_branch(current_user, branch_name)
- push_data = build_push_data(branch)
-
- EventCreateService.new.push(project, current_user, push_data)
- project.execute_hooks(push_data.dup, :push_hooks)
- project.execute_services(push_data.dup, :push_hooks)
-
+ # GitPushService handles execution of services and hooks for branch pushes
success('Branch was removed')
else
error('Failed to remove branch')
diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb
index e622fd5ea5d..ce79287e35a 100644
--- a/app/services/delete_user_service.rb
+++ b/app/services/delete_user_service.rb
@@ -5,18 +5,22 @@ class DeleteUserService
@current_user = current_user
end
- def execute(user)
- if user.solo_owned_groups.present?
+ def execute(user, options = {})
+ if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
- user
- else
- user.personal_projects.each do |project|
- # Skip repository removal because we remove directory with namespace
- # that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
- end
+ return user
+ end
+
+ user.solo_owned_groups.each do |group|
+ DestroyGroupService.new(group, current_user).execute
+ end
- user.destroy
+ user.personal_projects.each do |project|
+ # Skip repository removal because we remove directory with namespace
+ # that contain all this repositories
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
end
+
+ user.destroy
end
end
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
index d929a676293..3c42ac61be4 100644
--- a/app/services/destroy_group_service.rb
+++ b/app/services/destroy_group_service.rb
@@ -6,12 +6,12 @@ class DestroyGroupService
end
def execute
- @group.projects.each do |project|
+ group.projects.each do |project|
# Skip repository removal because we remove directory with namespace
# that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
end
- @group.destroy
+ group.destroy
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index d7ea30bc315..14e2a2c0699 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -1,10 +1,10 @@
-class GitPushService
- attr_accessor :project, :user, :push_data, :push_commits
+class GitPushService < BaseService
+ attr_accessor :push_data, :push_commits
include Gitlab::CurrentSettings
include Gitlab::Access
# This method will be called after each git update
- # and only if the provided user and project is present in GitLab.
+ # and only if the provided user and project are present in GitLab.
#
# All callbacks for post receive action should be placed here.
#
@@ -12,65 +12,93 @@ class GitPushService
# 1. Creates the push event
# 2. Updates merge requests
# 3. Recognizes cross-references from commit messages
- # 4. Executes the project's web hooks
+ # 4. Executes the project's webhooks
# 5. Executes the project's services
+ # 6. Checks if the project's main language has changed
#
- def execute(project, user, oldrev, newrev, ref)
- @project, @user = project, user
+ def execute
+ @project.repository.after_push_commit(branch_name, params[:newrev])
- project.repository.expire_cache
-
- if push_remove_branch?(ref, newrev)
+ if push_remove_branch?
+ @project.repository.after_remove_branch
@push_commits = []
- elsif push_to_new_branch?(ref, oldrev)
+ elsif push_to_new_branch?
+ @project.repository.after_create_branch
+
# Re-find the pushed commits.
- if is_default_branch?(ref)
+ if is_default_branch?
# 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
+ process_default_branch
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)
+ @push_commits = @project.repository.commits_between(@project.default_branch, params[:newrev])
# don't process commits for the initial push to the default branch
- process_commit_messages(ref)
+ process_commit_messages
end
- elsif push_to_existing_branch?(ref, oldrev)
+ elsif push_to_existing_branch?
# Collect data for this git push
- @push_commits = project.repository.commits_between(oldrev, newrev)
- process_commit_messages(ref)
+ @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev])
+ process_commit_messages
end
-
+ # Checks if the main language has changed in the project and if so
+ # it updates it accordingly
+ update_main_language
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.
- project.update_merge_requests(oldrev, newrev, ref, @user)
+ update_merge_requests
- @push_data = build_push_data(oldrev, newrev, ref)
+ perform_housekeeping
+ end
+
+ def update_main_language
+ current_language = @project.repository.main_language
- EventCreateService.new.push(project, user, @push_data)
- project.execute_hooks(@push_data.dup, :push_hooks)
- project.execute_services(@push_data.dup, :push_hooks)
- CreateCommitBuildsService.new.execute(project, @user, @push_data)
- ProjectCacheWorker.perform_async(project.id)
+ unless current_language == @project.main_language
+ return @project.update_attributes(main_language: current_language)
+ end
+
+ true
end
protected
+ def update_merge_requests
+ @project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user)
+
+ EventCreateService.new.push(@project, current_user, build_push_data)
+ @project.execute_hooks(build_push_data.dup, :push_hooks)
+ @project.execute_services(build_push_data.dup, :push_hooks)
+ CreateCommitBuildsService.new.execute(@project, current_user, build_push_data)
+ ProjectCacheWorker.perform_async(@project.id)
+ end
+
+ def perform_housekeeping
+ housekeeping = Projects::HousekeepingService.new(@project)
+ housekeeping.increment!
+ housekeeping.execute if housekeeping.needed?
+ rescue Projects::HousekeepingService::LeaseTaken
+ end
+
+ def process_default_branch
+ @push_commits = project.repository.commits(params[:newrev])
+
+ # Ensure HEAD points to the default branch in case it is not master
+ 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
+ end
+
# Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched,
# close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables.
- def process_commit_messages(ref)
- is_default_branch = is_default_branch?(ref)
+ def process_commit_messages
+ is_default_branch = is_default_branch?
authors = Hash.new do |hash, commit|
email = commit.author_email
@@ -89,9 +117,11 @@ 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.
- closed_issues = commit.closes_issues(user)
+ closed_issues = commit.closes_issues(current_user)
closed_issues.each do |issue|
- Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
+ if can?(current_user, :update_issue, issue)
+ Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
+ end
end
end
@@ -99,34 +129,38 @@ class GitPushService
end
end
- def build_push_data(oldrev, newrev, ref)
- Gitlab::PushDataBuilder.
- build(project, user, oldrev, newrev, ref, push_commits)
+ def build_push_data
+ @push_data ||= Gitlab::PushDataBuilder.
+ build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits)
end
- def push_to_existing_branch?(ref, oldrev)
+ def push_to_existing_branch?
# Return if this is not a push to a branch (e.g. new commits)
- Gitlab::Git.branch_ref?(ref) && !Gitlab::Git.blank_ref?(oldrev)
+ Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev])
end
- def push_to_new_branch?(ref, oldrev)
- Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(oldrev)
+ def push_to_new_branch?
+ Gitlab::Git.branch_ref?(params[:ref]) && Gitlab::Git.blank_ref?(params[:oldrev])
end
- def push_remove_branch?(ref, newrev)
- Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(newrev)
+ def push_remove_branch?
+ Gitlab::Git.branch_ref?(params[:ref]) && Gitlab::Git.blank_ref?(params[:newrev])
end
- def push_to_branch?(ref)
- Gitlab::Git.branch_ref?(ref)
+ def push_to_branch?
+ Gitlab::Git.branch_ref?(params[:ref])
end
- def is_default_branch?(ref)
- Gitlab::Git.branch_ref?(ref) &&
- (Gitlab::Git.ref_name(ref) == project.default_branch || project.default_branch.nil?)
+ def is_default_branch?
+ Gitlab::Git.branch_ref?(params[:ref]) &&
+ (Gitlab::Git.ref_name(params[:ref]) == project.default_branch || project.default_branch.nil?)
end
def commit_user(commit)
- commit.author || user
+ commit.author || current_user
+ end
+
+ def branch_name
+ @branch_name ||= Gitlab::Git.ref_name(params[:ref])
end
end
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index 4144c7111d0..c88c7672805 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -2,7 +2,7 @@ class GitTagPushService
attr_accessor :project, :user, :push_data
def execute(project, user, oldrev, newrev, ref)
- project.repository.expire_cache
+ project.repository.before_push_tag
@project, @user = project, user
@push_data = build_push_data(oldrev, newrev, ref)
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 2556f06e2d3..18f76d3f650 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -11,7 +11,10 @@ class IssuableBaseService < BaseService
issuable, issuable.project, current_user, issuable.milestone)
end
- def create_labels_note(issuable, added_labels, removed_labels)
+ def create_labels_note(issuable, old_labels)
+ added_labels = issuable.labels - old_labels
+ removed_labels = old_labels - issuable.labels
+
SystemNoteService.change_label(
issuable, issuable.project, current_user, added_labels, removed_labels)
end
@@ -54,7 +57,7 @@ class IssuableBaseService < BaseService
if params.present? && issuable.update_attributes(params.merge(updated_by: current_user))
issuable.reset_events_cache
handle_common_system_notes(issuable, old_labels: old_labels)
- handle_changes(issuable)
+ handle_changes(issuable, old_labels: old_labels)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
end
@@ -71,7 +74,19 @@ class IssuableBaseService < BaseService
end
end
- def handle_common_system_notes(issuable, options = {})
+ def has_changes?(issuable, old_labels: [])
+ valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
+
+ attrs_changed = valid_attrs.any? do |attr|
+ issuable.previous_changes.include?(attr.to_s)
+ end
+
+ labels_changed = issuable.labels != old_labels
+
+ attrs_changed || labels_changed
+ end
+
+ def handle_common_system_notes(issuable, old_labels: [])
if issuable.previous_changes.include?('title')
create_title_change_note(issuable, issuable.previous_changes['title'].first)
end
@@ -80,9 +95,6 @@ class IssuableBaseService < BaseService
create_task_status_note(issuable)
end
- old_labels = options[:old_labels]
- if old_labels && (issuable.labels != old_labels)
- create_labels_note(issuable, issuable.labels - old_labels, old_labels - issuable.labels)
- end
+ create_labels_note(issuable, old_labels) if issuable.labels != old_labels
end
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index a1a20e47681..78254b49af3 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -3,6 +3,7 @@ module Issues
def execute(issue, commit = nil)
if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue)
+ todo_service.close_issue(issue, current_user)
return issue
end
@@ -10,6 +11,7 @@ module Issues
event_service.close_issue(issue, current_user)
create_note(issue, commit)
notification_service.close_issue(issue, current_user)
+ todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index bcb380d3215..10787e8873c 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -9,6 +9,7 @@ module Issues
if issue.save
issue.update_attributes(label_ids: label_params)
notification_service.new_issue(issue, current_user)
+ todo_service.new_issue(issue, current_user)
event_service.open_issue(issue, current_user)
issue.create_cross_references!(current_user)
execute_hooks(issue, 'open')
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index a55a04dd5e0..3563cbaa997 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -4,7 +4,16 @@ module Issues
update(issue)
end
- def handle_changes(issue)
+ def handle_changes(issue, old_labels: [])
+ if has_changes?(issue, old_labels: old_labels)
+ todo_service.mark_pending_todos_as_done(issue, current_user)
+ end
+
+ if issue.previous_changes.include?('title') ||
+ issue.previous_changes.include?('description')
+ todo_service.update_issue(issue, current_user)
+ end
+
if issue.previous_changes.include?('milestone_id')
create_milestone_note(issue)
end
@@ -12,6 +21,12 @@ module Issues
if issue.previous_changes.include?('assignee_id')
create_assignee_note(issue)
notification_service.reassigned_issue(issue, current_user)
+ todo_service.reassigned_issue(issue, current_user)
+ end
+
+ added_labels = issue.labels - old_labels
+ if added_labels.present?
+ notification_service.relabeled_issue(issue, added_labels, current_user)
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index a9b29f9654d..fa34753c4fd 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -5,9 +5,7 @@ module MergeRequests
# Set MR attributes
merge_request.can_be_created = false
- merge_request.compare_failed = false
merge_request.compare_commits = []
- merge_request.compare_diffs = []
merge_request.source_project = project unless merge_request.source_project
merge_request.target_project ||= (project.forked_from_project || project)
merge_request.target_branch ||= merge_request.target_project.default_branch
@@ -21,46 +19,49 @@ module MergeRequests
return build_failed(merge_request, message)
end
- compare_result = CompareService.new.execute(
+ compare = CompareService.new.execute(
merge_request.source_project,
merge_request.source_branch,
merge_request.target_project,
merge_request.target_branch,
)
- commits = compare_result.commits
+ commits = compare.commits
# 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.source_project)
merge_request.can_be_created = true
- merge_request.compare_failed = false
-
- # Try to collect diff for merge request.
- diffs = compare_result.diffs
-
- if diffs.present?
- merge_request.compare_diffs = diffs
-
- elsif diffs == false
- merge_request.can_be_created = false
- merge_request.compare_failed = true
- end
+ merge_request.compare = compare
else
merge_request.can_be_created = false
- merge_request.compare_failed = false
end
commits = merge_request.compare_commits
if commits && commits.count == 1
commit = commits.first
merge_request.title = commit.title
- merge_request.description = commit.description.try(:strip)
+ merge_request.description ||= commit.description.try(:strip)
else
merge_request.title = merge_request.source_branch.titleize.humanize
end
+ # When your branch name starts with an iid followed by a dash this pattern will
+ # be interpreted as the use wants to close that issue on this project
+ # Pattern example: 112-fix-mep-mep
+ # Will lead to appending `Closes #112` to the description
+ if match = merge_request.source_branch.match(/\A(\d+)-/)
+ iid = match[1]
+ closes_issue = "Closes ##{iid}"
+
+ if merge_request.description.present?
+ merge_request.description << closes_issue.prepend("\n")
+ else
+ merge_request.description = closes_issue
+ end
+ end
+
merge_request
end
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index 47454f9f0c2..27ee81fe3e7 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -9,6 +9,7 @@ module MergeRequests
event_service.close_mr(merge_request, current_user)
create_note(merge_request)
notification_service.close_mr(merge_request, current_user)
+ todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 009d5a6867e..33609d01f20 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -2,7 +2,7 @@ module MergeRequests
class CreateService < MergeRequests::BaseService
def execute
# @project is used to determine whether the user can set the merge request's
- # assignee, milestone and labels. Whether they can depends on their
+ # assignee, milestone and labels. Whether they can depends on their
# permissions on the target project.
source_project = @project
@project = Project.find(params[:target_project_id]) if params[:target_project_id]
@@ -18,6 +18,7 @@ module MergeRequests
merge_request.update_attributes(label_ids: label_params)
event_service.open_mr(merge_request, current_user)
notification_service.new_merge_request(merge_request, current_user)
+ todo_service.new_merge_request(merge_request, current_user)
merge_request.create_cross_references!(current_user)
execute_hooks(merge_request)
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index cabc3d8fabb..9a58383b398 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -34,7 +34,8 @@ module MergeRequests
committer: committer
}
- repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options)
+ commit_id = repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options)
+ merge_request.update(merge_commit_sha: commit_id)
rescue StandardError => e
merge_request.update(merge_error: "Something went wrong during merge")
Rails.logger.error(e.message)
@@ -44,7 +45,7 @@ module MergeRequests
def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
- if params[:should_remove_source_branch]
+ if params[:should_remove_source_branch].present?
DeleteBranchService.new(@merge_request.source_project, current_user).
execute(merge_request.source_branch)
end
diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb
index 5cf7404a493..d6af12f9739 100644
--- a/app/services/merge_requests/merge_when_build_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb
@@ -19,15 +19,19 @@ module MergeRequests
end
# Triggers the automatic merge of merge_request once the build succeeds
- def trigger(build)
- merge_requests = merge_request_from(build)
+ def trigger(commit_status)
+ merge_requests = merge_request_from(commit_status)
merge_requests.each do |merge_request|
next unless merge_request.merge_when_build_succeeds?
+ next unless merge_request.mergeable?
- if merge_request.ci_commit && merge_request.ci_commit.success? && merge_request.mergeable?
- MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
- end
+ ci_commit = merge_request.ci_commit
+ next unless ci_commit
+ next unless ci_commit.sha == commit_status.sha
+ next unless ci_commit.success?
+
+ MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
end
@@ -45,9 +49,16 @@ module MergeRequests
private
- def merge_request_from(build)
- merge_requests = @project.origin_merge_requests.opened.where(source_branch: build.ref).to_a
- merge_requests += @project.fork_merge_requests.opened.where(source_branch: build.ref).to_a
+ def merge_request_from(commit_status)
+ branches = commit_status.ref
+
+ # This is for ref-less builds
+ branches ||= @project.repository.branch_names_contains(commit_status.sha)
+
+ return [] if branches.blank?
+
+ merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
+ merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
merge_requests.uniq.select(&:source_project)
end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index 8f25c5e2496..ebb67c7db65 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -21,7 +21,9 @@ module MergeRequests
closed_issues = merge_request.closes_issues(current_user)
closed_issues.each do |issue|
- Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
+ if can?(current_user, :update_issue, issue)
+ Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
+ end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 5ff2cc03dda..477c64e7377 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -14,7 +14,16 @@ module MergeRequests
update(merge_request)
end
- def handle_changes(merge_request)
+ def handle_changes(merge_request, old_labels: [])
+ if has_changes?(merge_request, old_labels: old_labels)
+ todo_service.mark_pending_todos_as_done(merge_request, current_user)
+ end
+
+ if merge_request.previous_changes.include?('title') ||
+ merge_request.previous_changes.include?('description')
+ todo_service.update_merge_request(merge_request, current_user)
+ end
+
if merge_request.previous_changes.include?('target_branch')
create_branch_change_note(merge_request, 'target',
merge_request.previous_changes['target_branch'].first,
@@ -28,12 +37,22 @@ module MergeRequests
if merge_request.previous_changes.include?('assignee_id')
create_assignee_note(merge_request)
notification_service.reassigned_merge_request(merge_request, current_user)
+ todo_service.reassigned_merge_request(merge_request, current_user)
end
if merge_request.previous_changes.include?('target_branch') ||
merge_request.previous_changes.include?('source_branch')
merge_request.mark_as_unchecked
end
+
+ added_labels = merge_request.labels - old_labels
+ if added_labels.present?
+ notification_service.relabeled_merge_request(
+ merge_request,
+ added_labels,
+ current_user
+ )
+ end
end
def reopen_service
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index a8486e6a5a1..2bb312bb252 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -6,27 +6,12 @@ module Notes
note.system = false
if note.save
- notification_service.new_note(note)
-
- # Skip system notes, like status changes and cross-references and awards
- unless note.system || note.is_award
- event_service.leave_note(note, note.author)
- note.create_cross_references!
- execute_hooks(note)
- end
+ # Finish the harder work in the background
+ NewNoteWorker.perform_in(2.seconds, note.id, params)
+ TodoService.new.new_note(note, current_user)
end
note
end
-
- def hook_data(note)
- Gitlab::NoteDataBuilder.build(note, current_user)
- end
-
- def execute_hooks(note)
- note_data = hook_data(note)
- note.project.execute_hooks(note_data, :note_hooks)
- note.project.execute_services(note_data, :note_hooks)
- end
end
end
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
new file mode 100644
index 00000000000..e818f58d13c
--- /dev/null
+++ b/app/services/notes/post_process_service.rb
@@ -0,0 +1,28 @@
+module Notes
+ class PostProcessService
+ attr_accessor :note
+
+ def initialize(note)
+ @note = note
+ end
+
+ def execute
+ # Skip system notes, like status changes and cross-references and awards
+ unless @note.system || @note.is_award
+ EventCreateService.new.leave_note(@note, @note.author)
+ @note.create_cross_references!
+ execute_note_hooks
+ end
+ end
+
+ def hook_data
+ Gitlab::NoteDataBuilder.build(@note, @note.author)
+ end
+
+ def execute_note_hooks
+ note_data = hook_data
+ @note.project.execute_hooks(note_data, :note_hooks)
+ @note.project.execute_services(note_data, :note_hooks)
+ end
+ end
+end
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 72e2f78008d..1361b1e0300 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -7,6 +7,10 @@ module Notes
note.create_new_cross_references!(current_user)
note.reset_events_cache
+ if note.previous_changes.include?('note')
+ TodoService.new.update_note(note, current_user)
+ end
+
note
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index bdf7b3ad2bb..19a6779dea9 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -24,16 +24,17 @@ class NotificationService
end
end
- # When create an issue we should send next emails:
+ # When create an issue we should send an email to:
#
# * issue assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
+ # * watchers of the issue's labels
#
def new_issue(issue, current_user)
new_resource_email(issue, issue.project, 'new_issue_email')
end
- # When we close an issue we should send next emails:
+ # When we close an issue we should send an email to:
#
# * issue author if their notification level is not Disabled
# * issue assignee if their notification level is not Disabled
@@ -43,7 +44,7 @@ class NotificationService
close_resource_email(issue, issue.project, current_user, 'closed_issue_email')
end
- # When we reassign an issue we should send next emails:
+ # When we reassign an issue we should send an email to:
#
# * issue old assignee if their notification level is not Disabled
# * issue new assignee if their notification level is not Disabled
@@ -52,16 +53,25 @@ class NotificationService
reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email')
end
+ # When we add labels to an issue we should send an email to:
+ #
+ # * watchers of the issue's labels
+ #
+ def relabeled_issue(issue, added_labels, current_user)
+ relabeled_resource_email(issue, added_labels, current_user, 'relabeled_issue_email')
+ end
- # When create a merge request we should send next emails:
+ # When create a merge request we should send an email to:
#
# * mr assignee if their notification level is not Disabled
+ # * project team members with notification level higher then Participating
+ # * watchers of the mr's labels
#
def new_merge_request(merge_request, current_user)
new_resource_email(merge_request, merge_request.target_project, 'new_merge_request_email')
end
- # When we reassign a merge_request we should send next emails:
+ # When we reassign a merge_request we should send an email to:
#
# * merge_request old assignee if their notification level is not Disabled
# * merge_request assignee if their notification level is not Disabled
@@ -70,6 +80,14 @@ class NotificationService
reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email')
end
+ # When we add labels to a merge request we should send an email to:
+ #
+ # * watchers of the mr's labels
+ #
+ def relabeled_merge_request(merge_request, added_labels, current_user)
+ relabeled_resource_email(merge_request, added_labels, current_user, 'relabeled_merge_request_email')
+ end
+
def close_mr(merge_request, current_user)
close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email')
end
@@ -91,7 +109,8 @@ class NotificationService
reopen_resource_email(
merge_request,
merge_request.target_project,
- current_user, 'merge_request_status_email',
+ current_user,
+ 'merge_request_status_email',
'reopened'
)
end
@@ -348,19 +367,23 @@ class NotificationService
end
def add_subscribed_users(recipients, target)
- return recipients unless target.respond_to? :subscriptions
+ return recipients unless target.respond_to? :subscribers
+
+ recipients + target.subscribers
+ end
- subscriptions = target.subscriptions
+ def add_labels_subscribers(recipients, target, labels: nil)
+ return recipients unless target.respond_to? :labels
- if subscriptions.any?
- recipients + subscriptions.where(subscribed: true).map(&:user)
- else
- recipients
+ (labels || target.labels).each do |label|
+ recipients += label.subscribers
end
+
+ recipients
end
def new_resource_email(target, project, method)
- recipients = build_recipients(target, project, target.author)
+ recipients = build_recipients(target, project, target.author, action: :new)
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later
@@ -376,10 +399,10 @@ class NotificationService
end
def reassign_resource_email(target, project, current_user, method)
- previous_assignee_id = previous_record(target, "assignee_id")
+ previous_assignee_id = previous_record(target, 'assignee_id')
previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- recipients = build_recipients(target, project, current_user, [previous_assignee])
+ recipients = build_recipients(target, project, current_user, action: :reassign, previous_assignee: previous_assignee)
recipients.each do |recipient|
mailer.send(
@@ -392,6 +415,15 @@ class NotificationService
end
end
+ def relabeled_resource_email(target, labels, current_user, method)
+ recipients = build_relabeled_recipients(target, current_user, labels: labels)
+ label_names = labels.map(&:name)
+
+ recipients.each do |recipient|
+ mailer.send(method, recipient.id, target.id, label_names, current_user.id).deliver_later
+ end
+ end
+
def reopen_resource_email(target, project, current_user, method, status)
recipients = build_recipients(target, project, current_user)
@@ -400,21 +432,39 @@ class NotificationService
end
end
- def build_recipients(target, project, current_user, extra_recipients = nil)
+ def build_recipients(target, project, current_user, action: nil, previous_assignee: nil)
recipients = target.participants(current_user)
- recipients = recipients.concat(extra_recipients).compact.uniq if extra_recipients
-
recipients = add_project_watchers(recipients, project)
recipients = reject_mention_users(recipients, project)
- recipients = reject_muted_users(recipients, project)
+ # Re-assign is considered as a mention of the new assignee so we add the
+ # new assignee to the list of recipients after we rejected users with
+ # the "on mention" notification level
+ if action == :reassign
+ recipients << previous_assignee if previous_assignee
+ recipients << target.assignee
+ end
+
+ recipients = reject_muted_users(recipients, project)
recipients = add_subscribed_users(recipients, target)
+
+ if action == :new
+ recipients = add_labels_subscribers(recipients, target)
+ end
+
recipients = reject_unsubscribed_users(recipients, target)
recipients.delete(current_user)
- recipients
+ recipients.uniq
+ end
+
+ def build_relabeled_recipients(target, current_user, labels:)
+ recipients = add_labels_subscribers([], target, labels: labels)
+ recipients = reject_unsubscribed_users(recipients, target)
+ recipients.delete(current_user)
+ recipients.uniq
end
def mailer
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 7408e09ed1e..ba50305dbd5 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -1,11 +1,7 @@
module Projects
class AutocompleteService < BaseService
- def initialize(project)
- @project = project
- end
-
def issues
- @project.issues.opened.select([:iid, :title])
+ @project.issues.visible_to_user(current_user).opened.select([:iid, :title])
end
def merge_requests
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 28872c89259..df5054f08d7 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -6,15 +6,25 @@ module Projects
DELETED_FLAG = '+deleted'
+ def pending_delete!
+ project.update_attribute(:pending_delete, true)
+
+ ProjectDestroyWorker.perform_in(1.minute, project.id, current_user.id, params)
+ end
+
def execute
return false unless can?(current_user, :remove_project, project)
project.team.truncate
- project.repository.expire_cache unless project.empty_repo?
repo_path = project.path_with_namespace
wiki_path = repo_path + '.wiki'
+ # Flush the cache for both repositories. This has to be done _before_
+ # removing the physical repositories as some expiration code depends on
+ # Git data (e.g. a list of branch names).
+ flush_caches(project, wiki_path)
+
Project.transaction do
project.destroy!
@@ -64,5 +74,11 @@ module Projects
def removal_path(path)
"#{path}+#{project.id}#{DELETED_FLAG}"
end
+
+ def flush_caches(project, wiki_path)
+ project.repository.before_delete
+
+ Repository.new(wiki_path, project).before_delete
+ end
end
end
diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb
index 99f22293d0d..6386f57fb0d 100644
--- a/app/services/projects/download_service.rb
+++ b/app/services/projects/download_service.rb
@@ -16,13 +16,7 @@ module Projects
uploader.download!(@url)
uploader.store!
- filename = uploader.image? ? uploader.file.basename : uploader.file.filename
-
- {
- 'alt' => filename,
- 'url' => uploader.secure_url,
- 'is_image' => uploader.image?
- }
+ uploader.to_h
end
private
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
new file mode 100644
index 00000000000..bccd67d3dbf
--- /dev/null
+++ b/app/services/projects/housekeeping_service.rb
@@ -0,0 +1,47 @@
+# Projects::HousekeepingService class
+#
+# Used for git housekeeping
+#
+# Ex.
+# Projects::HousekeepingService.new(project).execute
+#
+module Projects
+ class HousekeepingService < BaseService
+ include Gitlab::ShellAdapter
+
+ LEASE_TIMEOUT = 3600
+
+ class LeaseTaken < StandardError
+ def to_s
+ "Somebody already triggered housekeeping for this project in the past #{LEASE_TIMEOUT / 60} minutes"
+ end
+ end
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ raise LeaseTaken if !try_obtain_lease
+
+ GitlabShellWorker.perform_async(:gc, @project.path_with_namespace)
+ ensure
+ @project.update_column(:pushes_since_gc, 0)
+ end
+
+ def needed?
+ @project.pushes_since_gc >= 10
+ end
+
+ def increment!
+ @project.increment!(:pushes_since_gc)
+ end
+
+ private
+
+ def try_obtain_lease
+ lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
+ lease.try_obtain
+ end
+ end
+end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
new file mode 100644
index 00000000000..2015897dd19
--- /dev/null
+++ b/app/services/projects/import_service.rb
@@ -0,0 +1,67 @@
+module Projects
+ class ImportService < BaseService
+ include Gitlab::ShellAdapter
+
+ class Error < StandardError; end
+
+ ALLOWED_TYPES = [
+ 'bitbucket',
+ 'fogbugz',
+ 'gitlab',
+ 'github',
+ 'google_code'
+ ]
+
+ def execute
+ if unknown_url?
+ # In this case, we only want to import issues, not a repository.
+ create_repository
+ else
+ import_repository
+ end
+
+ import_data
+
+ success
+ rescue Error => e
+ error(e.message)
+ end
+
+ private
+
+ def create_repository
+ unless project.create_repository
+ raise Error, 'The repository could not be created.'
+ end
+ end
+
+ def import_repository
+ begin
+ gitlab_shell.import_repository(project.path_with_namespace, project.import_url)
+ rescue Gitlab::Shell::Error => e
+ raise Error, e.message
+ end
+ end
+
+ def import_data
+ return unless has_importer?
+
+ unless importer.execute
+ raise Error, 'The remote data could not be imported.'
+ end
+ end
+
+ def has_importer?
+ ALLOWED_TYPES.include?(project.import_type)
+ end
+
+ def importer
+ class_name = "Gitlab::#{project.import_type.camelize}Import::Importer"
+ class_name.constantize.new(project)
+ end
+
+ def unknown_url?
+ project.import_url == Project::UNKNOWN_IMPORT_URL
+ end
+ end
+end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 64ea6dd42eb..2e734654466 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -55,6 +55,9 @@ module Projects
# Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+ project.old_path_with_namespace = old_path
+
+ SystemHooksService.new.execute_hooks_for(project, :transfer)
true
end
end
diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb
index 279550d6f4a..012e82a7704 100644
--- a/app/services/projects/upload_service.rb
+++ b/app/services/projects/upload_service.rb
@@ -10,13 +10,7 @@ module Projects
uploader = FileUploader.new(@project)
uploader.store!(@file)
- filename = uploader.image? ? uploader.file.basename : uploader.file.filename
-
- {
- alt: filename,
- url: uploader.secure_url,
- is_image: uploader.image?
- }
+ uploader.to_h
end
private
diff --git a/app/services/repair_ldap_blocked_user_service.rb b/app/services/repair_ldap_blocked_user_service.rb
new file mode 100644
index 00000000000..863cef7ff61
--- /dev/null
+++ b/app/services/repair_ldap_blocked_user_service.rb
@@ -0,0 +1,17 @@
+class RepairLdapBlockedUserService
+ attr_accessor :user
+
+ def initialize(user)
+ @user = user
+ end
+
+ def execute
+ user.block if ldap_hard_blocked?
+ end
+
+ private
+
+ def ldap_hard_blocked?
+ user.ldap_blocked? && !user.ldap_user?
+ end
+end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index e904cb6c6fc..aa9837038a6 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -10,9 +10,8 @@ module Search
group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group
- project_ids = projects.pluck(:id)
- Gitlab::SearchResults.new(project_ids, params[:search])
+ Gitlab::SearchResults.new(current_user, projects, params[:search])
end
end
end
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index f630c0a3790..4b500914cfb 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -7,7 +7,8 @@ module Search
end
def execute
- Gitlab::ProjectSearchResults.new(project.id,
+ Gitlab::ProjectSearchResults.new(current_user,
+ project,
params[:search],
params[:repository_ref])
end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 8ca0877321d..0b3e713e220 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -7,8 +7,9 @@ module Search
end
def execute
- snippet_ids = Snippet.accessible_to(current_user).pluck(:id)
- Gitlab::SnippetSearchResults.new(snippet_ids, params[:search])
+ snippets = Snippet.accessible_to(current_user)
+
+ Gitlab::SnippetSearchResults.new(snippets, params[:search])
end
end
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 8b5143e1eb7..ea2b26ccb52 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -18,7 +18,8 @@ class SystemHooksService
def build_event_data(model, event)
data = {
event_name: build_event_name(model, event),
- created_at: model.created_at.xmlschema
+ created_at: model.created_at.xmlschema,
+ updated_at: model.updated_at.xmlschema
}
case model
@@ -34,11 +35,20 @@ class SystemHooksService
end
when Project
data.merge!(project_data(model))
+
+ if event == :rename || event == :transfer
+ data.merge!({
+ old_path_with_namespace: model.old_path_with_namespace
+ })
+ end
+
+ data
when User
data.merge!({
name: model.name,
email: model.email,
- user_id: model.id
+ user_id: model.id,
+ username: model.username
})
when ProjectMember
data.merge!(project_member_data(model))
@@ -90,8 +100,10 @@ class SystemHooksService
project_path: model.project.path,
project_path_with_namespace: model.project.path_with_namespace,
project_id: model.project.id,
+ user_username: model.user.username,
user_name: model.user.name,
user_email: model.user.email,
+ user_id: model.user.id,
access_level: model.human_access,
project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase
}
@@ -102,6 +114,7 @@ class SystemHooksService
group_name: model.group.name,
group_path: model.group.path,
group_id: model.group.id,
+ user_username: model.user.username,
user_name: model.user.name,
user_email: model.user.email,
user_id: model.user.id,
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 98a71cbf1ad..f09b77c4a57 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -41,7 +41,7 @@ class SystemNoteService
#
# Returns the created Note object
def self.change_assignee(noteable, project, author, assignee)
- body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}"
+ body = assignee.nil? ? 'Assignee removed' : "Reassigned to #{assignee.to_reference}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -66,7 +66,7 @@ class SystemNoteService
def self.change_label(noteable, project, author, added_labels, removed_labels)
labels_count = added_labels.count + removed_labels.count
- references = ->(label) { "~#{label.id}" }
+ references = ->(label) { label.to_reference(format: :id) }
added_labels = added_labels.map(&references).join(' ')
removed_labels = removed_labels.map(&references).join(' ')
@@ -103,7 +103,7 @@ class SystemNoteService
# Returns the created Note object
def self.change_milestone(noteable, project, author, milestone)
body = 'Milestone '
- body += milestone.nil? ? 'removed' : "changed to #{milestone.title}"
+ body += milestone.nil? ? 'removed' : "changed to #{milestone.to_reference(project)}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -207,6 +207,18 @@ class SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ # Called when a branch is created from the 'new branch' button on a issue
+ # Example note text:
+ #
+ # "Started branch `201-issue-branch-button`"
+ def self.new_issue_branch(issue, project, author, branch)
+ h = Gitlab::Application.routes.url_helpers
+ link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
+
+ body = "Started branch [`#{branch}`](#{link})"
+ create_note(noteable: issue, project: project, author: author, note: body)
+ end
+
# Called when a Mentionable references a Noteable
#
# noteable - Noteable object being referenced
@@ -274,12 +286,15 @@ class SystemNoteService
# Check if a cross reference to a noteable from a mentioner already exists
#
# This method is used to prevent multiple notes being created for a mention
- # when a issue is updated, for example.
+ # when a issue is updated, for example. The method also calls notes_for_mentioner
+ # to check if the mentioner is a commit, and return matches only on commit hash
+ # instead of project + commit, to avoid repeated mentions from forks.
#
# noteable - Noteable object being referenced
# mentioner - Mentionable object
#
# Returns Boolean
+
def self.cross_reference_exists?(noteable, mentioner)
# Initial scope should be system notes of this noteable type
notes = Note.system.where(noteable_type: noteable.class)
@@ -291,14 +306,20 @@ class SystemNoteService
notes = notes.where(noteable_id: noteable.id)
end
- gfm_reference = mentioner.gfm_reference(noteable.project)
- notes = notes.where(note: cross_reference_note_content(gfm_reference))
-
- notes.count > 0
+ notes_for_mentioner(mentioner, noteable, notes).count > 0
end
private
+ def self.notes_for_mentioner(mentioner, noteable, notes)
+ if mentioner.is_a?(Commit)
+ notes.where('note LIKE ?', "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}")
+ else
+ gfm_reference = mentioner.gfm_reference(noteable.project)
+ notes.where(note: cross_reference_note_content(gfm_reference))
+ end
+ end
+
def self.create_note(args = {})
Note.create(args.merge(system: true))
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
new file mode 100644
index 00000000000..4392e2d17fe
--- /dev/null
+++ b/app/services/todo_service.rb
@@ -0,0 +1,170 @@
+# TodoService class
+#
+# Used for creating todos after certain user actions
+#
+# Ex.
+# TodoService.new.new_issue(issue, current_user)
+#
+class TodoService
+ # When create an issue we should:
+ #
+ # * create a todo for assignee if issue is assigned
+ # * create a todo for each mentioned user on issue
+ #
+ def new_issue(issue, current_user)
+ new_issuable(issue, current_user)
+ end
+
+ # When update an issue we should:
+ #
+ # * mark all pending todos related to the issue for the current user as done
+ #
+ def update_issue(issue, current_user)
+ create_mention_todos(issue.project, issue, current_user)
+ end
+
+ # When close an issue we should:
+ #
+ # * mark all pending todos related to the target for the current user as done
+ #
+ def close_issue(issue, current_user)
+ mark_pending_todos_as_done(issue, current_user)
+ end
+
+ # When we reassign an issue we should:
+ #
+ # * create a pending todo for new assignee if issue is assigned
+ #
+ def reassigned_issue(issue, current_user)
+ create_assignment_todo(issue, current_user)
+ end
+
+ # When create a merge request we should:
+ #
+ # * creates a pending todo for assignee if merge request is assigned
+ # * create a todo for each mentioned user on merge request
+ #
+ def new_merge_request(merge_request, current_user)
+ new_issuable(merge_request, current_user)
+ end
+
+ # When update a merge request we should:
+ #
+ # * create a todo for each mentioned user on merge request
+ #
+ def update_merge_request(merge_request, current_user)
+ create_mention_todos(merge_request.project, merge_request, current_user)
+ end
+
+ # When close a merge request we should:
+ #
+ # * mark all pending todos related to the target for the current user as done
+ #
+ def close_merge_request(merge_request, current_user)
+ mark_pending_todos_as_done(merge_request, current_user)
+ end
+
+ # When we reassign a merge request we should:
+ #
+ # * creates a pending todo for new assignee if merge request is assigned
+ #
+ def reassigned_merge_request(merge_request, current_user)
+ create_assignment_todo(merge_request, current_user)
+ end
+
+ # When merge a merge request we should:
+ #
+ # * mark all pending todos related to the target for the current user as done
+ #
+ def merge_merge_request(merge_request, current_user)
+ mark_pending_todos_as_done(merge_request, current_user)
+ end
+
+ # When create a note we should:
+ #
+ # * mark all pending todos related to the noteable for the note author as done
+ # * create a todo for each mentioned user on note
+ #
+ def new_note(note, current_user)
+ handle_note(note, current_user)
+ end
+
+ # When update a note we should:
+ #
+ # * mark all pending todos related to the noteable for the current user as done
+ # * create a todo for each new user mentioned on note
+ #
+ def update_note(note, current_user)
+ handle_note(note, current_user)
+ end
+
+ # When marking pending todos as done we should:
+ #
+ # * mark all pending todos related to the target for the current user as done
+ #
+ def mark_pending_todos_as_done(target, user)
+ pending_todos(user, target.project, target).update_all(state: :done)
+ end
+
+ private
+
+ def create_todos(project, target, author, users, action, note = nil)
+ Array(users).each do |user|
+ next if pending_todos(user, project, target).exists?
+
+ Todo.create(
+ project: project,
+ user_id: user.id,
+ author_id: author.id,
+ target_id: target.id,
+ target_type: target.class.name,
+ action: action,
+ note: note
+ )
+ end
+ end
+
+ def new_issuable(issuable, author)
+ create_assignment_todo(issuable, author)
+ create_mention_todos(issuable.project, issuable, author)
+ end
+
+ def handle_note(note, author)
+ # Skip system notes, notes on commit, and notes on project snippet
+ return if note.system? || ['Commit', 'Snippet'].include?(note.noteable_type)
+
+ project = note.project
+ target = note.noteable
+
+ mark_pending_todos_as_done(target, author)
+ create_mention_todos(project, target, author, note)
+ end
+
+ def create_assignment_todo(issuable, author)
+ if issuable.assignee && issuable.assignee != author
+ create_todos(issuable.project, issuable, author, issuable.assignee, Todo::ASSIGNED)
+ end
+ end
+
+ def create_mention_todos(project, issuable, author, note = nil)
+ mentioned_users = filter_mentioned_users(project, note || issuable, author)
+ create_todos(project, issuable, author, mentioned_users, Todo::MENTIONED, note)
+ end
+
+ def filter_mentioned_users(project, target, author)
+ mentioned_users = target.mentioned_users.select do |user|
+ user.can?(:read_project, project)
+ end
+
+ mentioned_users.delete(author)
+ mentioned_users.uniq
+ end
+
+ def pending_todos(user, project, target)
+ user.todos.pending.where(
+ project_id: project.id,
+ target_id: target.id,
+ target_type: target.class.name
+ )
+ end
+end
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index 1b0ae6c0056..1cd93263c9f 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -32,6 +32,10 @@ class ArtifactUploader < CarrierWave::Uploader::Base
self.class.storage == CarrierWave::Storage::File
end
+ def filename
+ file.try(:filename)
+ end
+
def exists?
file.try(:exists?)
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index ac920119a85..86d24469e05 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -30,4 +30,19 @@ class FileUploader < CarrierWave::Uploader::Base
def secure_url
File.join("/uploads", @secret, file.filename)
end
+
+ def to_h
+ filename = image? ? self.file.basename : self.file.filename
+ escaped_filename = filename.gsub("]", "\\]")
+
+ markdown = "[#{escaped_filename}](#{self.secure_url})"
+ markdown.prepend("!") if image?
+
+ {
+ alt: filename,
+ url: self.secure_url,
+ is_image: image?,
+ markdown: markdown
+ }
+ end
end
diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb
index b35af100803..aab07a7ece4 100644
--- a/app/validators/email_validator.rb
+++ b/app/validators/email_validator.rb
@@ -1,18 +1,5 @@
-# EmailValidator
-#
-# Based on https://github.com/balexand/email_validator
-#
-# Extended to use only strict mode with following allowed characters:
-# ' - apostrophe
-#
-# See http://www.remote.org/jochen/mail/info/chars.html
-#
class EmailValidator < ActiveModel::EachValidator
- PATTERN = /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i.freeze
-
def validate_each(record, attribute, value)
- unless value =~ PATTERN
- record.errors.add(attribute, options[:message] || :invalid)
- end
+ record.errors.add(attribute, :invalid) unless value =~ Devise.email_regexp
end
end
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index 10e35ce665a..7a35958cc5f 100644
--- a/app/validators/namespace_validator.rb
+++ b/app/validators/namespace_validator.rb
@@ -17,6 +17,7 @@ class NamespaceValidator < ActiveModel::EachValidator
hooks
issues
merge_requests
+ new
notes
profile
projects
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
index 2848b9cd33d..a77beb2683d 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -29,8 +29,11 @@ class UrlValidator < ActiveModel::EachValidator
end
def valid_url?(value)
+ return false if value.nil?
+
options = default_options.merge(self.options)
+ value.strip!
value =~ /\A#{URI.regexp(options[:protocols])}\z/
end
end
diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml
index 619533e09a7..2741eb44357 100644
--- a/app/views/abuse_report_mailer/notify.html.haml
+++ b/app/views/abuse_report_mailer/notify.html.haml
@@ -8,4 +8,4 @@
= @abuse_report.message
%p
- = link_to "View details", abuse_reports_url
+ = link_to "View details", admin_abuse_reports_url
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index cffd7684008..3bc1b24b5e2 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -2,7 +2,7 @@
%h3.page-title Report abuse
%p Please use this form to report users who create spam issues, comments or behave inappropriately.
%hr
-= form_for @abuse_report, html: { class: 'form-horizontal'} do |f|
+= form_for @abuse_report, html: { class: 'form-horizontal js-quick-submit js-requires-input'} do |f|
= f.hidden_field :user_id
- if @abuse_report.errors.any?
.alert.alert-danger
@@ -16,7 +16,7 @@
.form-group
= f.label :message, class: 'control-label'
.col-sm-10
- = f.text_area :message, class: "form-control", rows: 2, required: true
+ = f.text_area :message, class: "form-control", rows: 2, required: true, value: sanitize(@ref_url)
.help-block
Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment.
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index d3afc658cd6..2ab01704b77 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -2,25 +2,30 @@
- user = abuse_report.user
%tr
%td
- - if reporter
- = link_to reporter.name, reporter
+ - if user
+ = link_to user.name, [:admin, user]
+ .light.small
+ Joined #{time_ago_with_tooltip(user.created_at)}
- else
(removed)
%td
- = abuse_report.created_at.to_s(:short)
- %td
- = abuse_report.message
- %td
- - if user
- = link_to user.name, user
+ - if reporter
+ = link_to reporter.name, [:admin, reporter]
- else
(removed)
+ .light.small
+ = time_ago_with_tooltip(abuse_report.created_at)
+ %td
+ = markdown(abuse_report.message.squish!, pipeline: :single_line)
%td
- if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-xs btn-remove js-remove-tr"
%td
- - if user
+ - if user && !user.blocked?
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
+ - else
+ .btn.btn-xs.disabled
+ Already Blocked
= link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr"
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 40a5fe4628b..bc4a9cedb2c 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -6,10 +6,9 @@
%table.table
%thead
%tr
+ %th User
%th Reported by
- %th Reported at
%th Message
- %th User
%th Primary action
%th
= render @abuse_reports
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
new file mode 100644
index 00000000000..6f325914d14
--- /dev/null
+++ b/app/views/admin/appearances/_form.html.haml
@@ -0,0 +1,58 @@
+= form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f|
+ - if @appearance.errors.any?
+ .alert.alert-danger
+ - @appearance.errors.full_messages.each do |msg|
+ %p= msg
+
+ %fieldset.sign-in
+ %legend
+ Sign in/Sign up pages:
+ .form-group
+ = f.label :title, class: 'control-label'
+ .col-sm-10
+ = f.text_field :title, class: "form-control"
+ .form-group
+ = f.label :description, class: 'control-label'
+ .col-sm-10
+ = f.text_area :description, class: "form-control", rows: 10
+ .hint
+ Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown', 'markdown'), target: '_blank'}.
+ .form-group
+ = f.label :logo, class: 'control-label'
+ .col-sm-10
+ - if @appearance.logo?
+ = image_tag @appearance.logo_url, class: 'appearance-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo"
+ %hr
+ = f.hidden_field :logo_cache
+ = f.file_field :logo, class: ""
+ .hint
+ Maximum file size is 1MB. Pages are optimized for a 640x360 px logo.
+
+ %fieldset.app_logo
+ %legend
+ Navigation bar:
+ .form-group
+ = f.label :header_logo, 'Header logo', class: 'control-label'
+ .col-sm-10
+ - if @appearance.header_logo?
+ = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo"
+ %hr
+ = f.hidden_field :header_logo_cache
+ = f.file_field :header_logo, class: ""
+ .hint
+ Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo
+
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save'
+ - if @appearance.persisted?
+ = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank'
+
+ - if @appearance.updated_at
+ %span.pull-right
+ Last edit #{time_ago_with_tooltip(@appearance.updated_at)}
diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml
new file mode 100644
index 00000000000..dd4a64e80bc
--- /dev/null
+++ b/app/views/admin/appearances/preview.html.haml
@@ -0,0 +1,29 @@
+- page_title "Preview | Appearance"
+%h3.page-title
+ Appearance settings - Preview
+%hr
+
+.ui-box
+ .title
+ Sign-in page
+ %div
+ .login-page
+ .container
+ .content
+ .login-title
+ %h1= brand_title
+ %hr
+ .container
+ .content
+ .row
+ .col-sm-7
+ .brand-image
+ = brand_image
+ .brand_text
+ = brand_text
+ .col-sm-4
+ .login-box
+ %h3.page-title Sign in
+ = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email"
+ = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password"
+ = button_tag "Sign in", class: "btn-create btn"
diff --git a/app/views/admin/appearances/show.html.haml b/app/views/admin/appearances/show.html.haml
new file mode 100644
index 00000000000..089e8e4cb7a
--- /dev/null
+++ b/app/views/admin/appearances/show.html.haml
@@ -0,0 +1,7 @@
+- page_title "Appearance"
+%h3.page-title
+ Appearance settings
+%p.light
+ You can modify the look and feel of GitLab here
+
+= render 'form'
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 89b38a0dad0..b30dfd109ea 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -14,11 +14,11 @@
.form-group.project-visibility-level-holder
= f.label :default_project_visibility, class: 'control-label col-sm-2'
.col-sm-10
- = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project)
+ = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
.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: PersonalSnippet)
+ = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
.form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-10
@@ -48,6 +48,16 @@
= f.check_box :version_check_enabled
Version check enabled
.form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :email_author_in_body do
+ = f.check_box :email_author_in_body
+ Include author name in notification email body
+ .help-block
+ Some email servers do not support overriding the email sender name.
+ Enable this option to include the name of the author of the issue,
+ merge request or comment in the email body instead.
+ .form-group
= f.label :admin_notification_email, class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :admin_notification_email, class: 'form-control'
@@ -105,14 +115,14 @@
= f.check_box :signin_enabled
Sign-in enabled
.form-group
- = f.label :two_factor_authentication, 'Two-Factor authentication', class: 'control-label col-sm-2'
+ = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :require_two_factor_authentication do
= f.check_box :require_two_factor_authentication
- Require all users to setup Two-Factor authentication
+ Require all users to setup Two-factor authentication
.form-group
- = f.label :two_factor_authentication, 'Two-Factor grace period (hours)', class: 'control-label col-sm-2'
+ = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
.help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
@@ -180,14 +190,6 @@
sending messages to this port, without it metrics data will not be
saved.
.form-group
- = f.label :metrics_username, 'InfluxDB username', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :metrics_username, class: 'form-control'
- .form-group
- = f.label :metrics_password, 'InfluxDB password', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :metrics_password, class: 'form-control'
- .form-group
= f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :metrics_pool_size, class: 'form-control'
@@ -210,6 +212,13 @@
.help-block
A method call is only tracked when it takes longer to complete than
the given amount of milliseconds.
+ .form-group
+ = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_sample_interval, class: 'form-control'
+ .help-block
+ The sampling interval in seconds. Sampled data includes memory usage,
+ retained Ruby objects, file descriptors and so on.
%fieldset
%legend Spam and Anti-bot Protection
@@ -219,19 +228,55 @@
= f.label :recaptcha_enabled do
= f.check_box :recaptcha_enabled
Enable reCAPTCHA
- %span.help-block#recaptcha_help_block Helps preventing bots from creating accounts
+ %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
.form-group
= f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :recaptcha_site_key, class: 'form-control'
.help-block
- Generate site and private keys here:
+ Generate site and private keys at
%a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha
+
.form-group
= f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :recaptcha_private_key, class: 'form-control'
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :akismet_enabled do
+ = f.check_box :akismet_enabled
+ Enable Akismet
+ %span.help-block#akismet_help_block Helps prevent bots from creating issues
+
+ .form-group
+ = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :akismet_api_key, class: 'form-control'
+ .help-block
+ Generate API key at
+ %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com
+
+ %fieldset
+ %legend Error Reporting and Logging
+ %p
+ These settings require a restart to take effect.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :sentry_enabled do
+ = f.check_box :sentry_enabled
+ Enable Sentry
+ .help-block
+ Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
+ %a{ href: 'https://getsentry.com', target: '_blank' } https://getsentry.com
+
+ .form-group
+ = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :sentry_dsn, class: 'form-control'
+
.form-actions
- = f.submit 'Save', class: 'btn btn-primary'
+ = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index fa4e6335c73..e18f7b499dd 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -22,5 +22,5 @@
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
.form-actions
- = f.submit 'Submit', class: "btn btn-primary wide"
+ = f.submit 'Submit', class: "btn btn-save wide"
= link_to "Cancel", admin_applications_path, class: "btn btn-default"
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
new file mode 100644
index 00000000000..b748460a9f7
--- /dev/null
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -0,0 +1,40 @@
+.broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) }
+ = icon('bullhorn')
+ .js-broadcast-message-preview
+ = render_broadcast_message(@broadcast_message.message.presence || "Your message here")
+
+= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
+ -if @broadcast_message.errors.any?
+ .alert.alert-danger
+ - @broadcast_message.errors.full_messages.each do |msg|
+ %p= msg
+ .form-group
+ = f.label :message, class: 'control-label'
+ .col-sm-10
+ = f.text_area :message, class: "form-control js-autosize",
+ required: true,
+ data: { preview_path: preview_admin_broadcast_messages_path }
+ .form-group.js-toggle-colors-container
+ .col-sm-10.col-sm-offset-2
+ = link_to 'Customize colors', '#', class: 'js-toggle-colors-link'
+ .form-group.js-toggle-colors-container.hide
+ = f.label :color, "Background Color", class: 'control-label'
+ .col-sm-10
+ = f.color_field :color, class: "form-control"
+ .form-group.js-toggle-colors-container.hide
+ = f.label :font, "Font Color", class: 'control-label'
+ .col-sm-10
+ = f.color_field :font, class: "form-control"
+ .form-group
+ = f.label :starts_at, class: 'control-label'
+ .col-sm-10.datetime-controls
+ = f.datetime_select :starts_at, {}, class: 'form-control form-control-inline'
+ .form-group
+ = f.label :ends_at, class: 'control-label'
+ .col-sm-10.datetime-controls
+ = f.datetime_select :ends_at, {}, class: 'form-control form-control-inline'
+ .form-actions
+ - if @broadcast_message.persisted?
+ = f.submit "Update broadcast message", class: "btn btn-create"
+ - else
+ = f.submit "Add broadcast message", class: "btn btn-create"
diff --git a/app/views/admin/broadcast_messages/edit.html.haml b/app/views/admin/broadcast_messages/edit.html.haml
new file mode 100644
index 00000000000..45e053eb31d
--- /dev/null
+++ b/app/views/admin/broadcast_messages/edit.html.haml
@@ -0,0 +1,3 @@
+- page_title "Broadcast Messages"
+
+= render 'form'
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index 17dffebd360..c05538a393c 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -1,60 +1,37 @@
- page_title "Broadcast Messages"
+
%h3.page-title
Broadcast Messages
%p.light
- Broadcast messages are displayed for every user and can be used to notify users about scheduled maintenance, recent upgrades and more.
-.broadcast-message-preview
- %i.fa.fa-bullhorn
- %span Your message here
-
-= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal'} do |f|
- -if @broadcast_message.errors.any?
- .alert.alert-danger
- - @broadcast_message.errors.full_messages.each do |msg|
- %p= msg
- .form-group
- = f.label :message, class: 'control-label'
- .col-sm-10
- = f.text_area :message, class: "form-control", rows: 2, required: true
- %div
- = link_to '#', class: 'js-toggle-colors-link' do
- Customize colors
- .form-group.js-toggle-colors-container.hide
- = f.label :color, "Background Color", class: 'control-label'
- .col-sm-10
- = f.color_field :color, value: "#eb9532", class: "form-control"
- .form-group.js-toggle-colors-container.hide
- = f.label :font, "Font Color", class: 'control-label'
- .col-sm-10
- = f.color_field :font, value: "#FFFFFF", class: "form-control"
- .form-group
- = f.label :starts_at, class: 'control-label'
- .col-sm-10.datetime-controls
- = f.datetime_select :starts_at
- .form-group
- = f.label :ends_at, class: 'control-label'
- .col-sm-10.datetime-controls
- = f.datetime_select :ends_at
- .form-actions
- = f.submit "Add broadcast message", class: "btn btn-create"
+ Broadcast messages are displayed for every user and can be used to notify
+ users about scheduled maintenance, recent upgrades and more.
--if @broadcast_messages.any?
- %ul.bordered-list.broadcast-messages
- - @broadcast_messages.each do |broadcast_message|
- %li
- .pull-right
- - if broadcast_message.starts_at
- %strong
- #{broadcast_message.starts_at.to_s(:short)}
- \...
- - if broadcast_message.ends_at
- %strong
- #{broadcast_message.ends_at.to_s(:short)}
- &nbsp;
- = link_to [:admin, broadcast_message], method: :delete, remote: true, class: 'remove-row btn btn-xs' do
- %i.fa.fa-times.cred
+= render 'form'
- .message= broadcast_message.message
+%br.clearfix
+-if @broadcast_messages.any?
+ %table.table
+ %thead
+ %tr
+ %th Status
+ %th Preview
+ %th Starts
+ %th Ends
+ %th &nbsp;
+ %tbody
+ - @broadcast_messages.each do |message|
+ %tr
+ %td
+ = broadcast_message_status(message)
+ %td
+ = broadcast_message(message)
+ %td
+ = message.starts_at
+ %td
+ = message.ends_at
+ %td
+ = link_to icon('pencil-square-o'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-xs'
+ = link_to icon('times'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-xs btn-danger'
- = paginate @broadcast_messages
+ = paginate @broadcast_messages, theme: 'gitlab'
diff --git a/app/views/admin/broadcast_messages/preview.js.haml b/app/views/admin/broadcast_messages/preview.js.haml
new file mode 100644
index 00000000000..fbc9453c72e
--- /dev/null
+++ b/app/views/admin/broadcast_messages/preview.js.haml
@@ -0,0 +1 @@
+$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@message))}");
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 6936e614346..588ad767426 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -4,13 +4,13 @@
= ci_status_with_icon(build.status)
%td.build-link
- - if build.target_url
- = link_to build.target_url do
+ - if can?(current_user, :read_build, build.project)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
%strong Build ##{build.id}
- else
%strong Build ##{build.id}
- - if build.show_warning?
+ - if build.stuck?
%i.fa.fa-warning.text-warning
%td
@@ -18,11 +18,11 @@
= link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace"
%td
- = link_to build.short_sha, namespace_project_commit_path(project.namespace, project, build.sha), class: "monospace"
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
%td
- if build.ref
- = link_to build.ref, namespace_project_commits_path(project.namespace, project, build.ref)
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
- else
.light none
@@ -60,14 +60,13 @@
%td
.pull-right
- - if current_user && can?(current_user, :download_build_artifacts, project) && build.download_url
- = link_to build.download_url, title: 'Download artifacts' do
+ - if can?(current_user, :read_build, project) && build.artifacts?
+ = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
%i.fa.fa-download
- - if current_user && can?(current_user, :manage_builds, build.project)
+ - if can?(current_user, :update_build, build.project)
- if build.active?
- - if build.cancel_url
- = link_to build.cancel_url, method: :post, title: 'Cancel' do
- %i.fa.fa-remove.cred
- - elsif defined?(allow_retry) && allow_retry && build.retry_url
- = link_to build.retry_url, method: :post, title: 'Retry' do
+ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
+ %i.fa.fa-remove.cred
+ - elsif defined?(allow_retry) && allow_retry && build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
%i.fa.fa-repeat
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 55da06a7fe9..5931efdefe6 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -1,26 +1,25 @@
-.project-issuable-filter
- .controls
- .pull-left.hidden-xs
- - if @all_builds.running_or_pending.any?
- = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
-
- %ul.center-top-menu
+.top-area
+ %ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to admin_builds_path do
+ All
+ %span.badge.js-totalbuilds-count= @all_builds.count(:id)
+
+ %li{class: ('active' if @scope == 'running')}
+ = link_to admin_builds_path(scope: :running) do
Running
- %span.badge.js-running-count= @all_builds.running_or_pending.count(:id)
+ %span.badge.js-running-count= number_with_delimiter(@all_builds.running_or_pending.count(:id))
%li{class: ('active' if @scope == 'finished')}
= link_to admin_builds_path(scope: :finished) do
Finished
- %span.badge.js-running-count= @all_builds.finished.count(:id)
+ %span.badge.js-running-count= number_with_delimiter(@all_builds.finished.count(:id))
- %li{class: ('active' if @scope == 'all')}
- = link_to admin_builds_path(scope: :all) do
- All
- %span.badge.js-totalbuilds-count= @all_builds.count(:id)
+ .nav-controls
+ - if @all_builds.running_or_pending.any?
+ = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
-.gray-content-block
+.gray-content-block.second-block
#{(@scope || 'running').capitalize} builds
%ul.content-list
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 531247e9148..3274ba5377b 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -6,35 +6,35 @@
%p
Forks
%span.light.pull-right
- = ForkedProjectLink.count
+ = number_with_delimiter(ForkedProjectLink.count)
%p
Issues
%span.light.pull-right
- = Issue.count
+ = number_with_delimiter(Issue.count)
%p
Merge Requests
%span.light.pull-right
- = MergeRequest.count
+ = number_with_delimiter(MergeRequest.count)
%p
Notes
%span.light.pull-right
- = Note.count
+ = number_with_delimiter(Note.count)
%p
Snippets
%span.light.pull-right
- = Snippet.count
+ = number_with_delimiter(Snippet.count)
%p
SSH Keys
%span.light.pull-right
- = Key.count
+ = number_with_delimiter(Key.count)
%p
Milestones
%span.light.pull-right
- = Milestone.count
+ = number_with_delimiter(Milestone.count)
%p
Active Users
%span.light.pull-right
- = User.active.count
+ = number_with_delimiter(User.active.count)
.col-md-4
%h4
Features
@@ -92,6 +92,11 @@
Rails
%span.pull-right
#{Rails::VERSION::STRING}
+
+ %p
+ = Gitlab::Database.adapter_name
+ %span.pull-right
+ = Gitlab::Database.version
%hr
.row
.col-sm-4
@@ -99,7 +104,7 @@
%h4 Projects
.data
= link_to admin_namespaces_projects_path do
- %h1= Project.count
+ %h1= number_with_delimiter(Project.count)
%hr
= link_to('New Project', new_project_path, class: "btn btn-new")
.col-sm-4
@@ -107,7 +112,7 @@
%h4 Users
.data
= link_to admin_users_path do
- %h1= User.count
+ %h1= number_with_delimiter(User.count)
%hr
= link_to 'New User', new_admin_user_path, class: "btn btn-new"
.col-sm-4
@@ -115,7 +120,7 @@
%h4 Groups
.data
= link_to admin_groups_path do
- %h1= Group.count
+ %h1= number_with_delimiter(Group.count)
%hr
= link_to 'New Group', new_admin_group_path, class: "btn btn-new"
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index 841e6971fb2..41c43899978 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -2,7 +2,7 @@
.panel.panel-default
.panel-heading
Public deploy keys (#{@deploy_keys.count})
- .panel-head-actions
+ .controls
= link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm"
- if @deploy_keys.any?
.table-holder
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 8de2ba74a79..198026a1f75 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -21,6 +21,5 @@
- else
.form-actions
- = f.submit 'Save changes', class: "btn btn-primary"
+ = f.submit 'Save changes', class: "btn btn-save"
= link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel"
-
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 5ce7cdf2f8d..118d3cfea07 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Groups"
%h3.page-title
- Groups (#{@groups.total_count})
+ Groups (#{number_with_delimiter(@groups.total_count)})
= link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right"
%p.light
@@ -17,7 +17,7 @@
.pull-right
.dropdown.inline
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %span.light sort:
+ %span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 296497a4cd4..264fa1bf0cd 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -30,7 +30,7 @@
%li
%span.light Created on:
%strong
- = @group.created_at.stamp("March 1, 1999")
+ = @group.created_at.to_s(:medium)
.panel.panel-default
.panel-heading
@@ -50,6 +50,22 @@
.panel-footer
= paginate @projects, param_name: 'projects_page', theme: 'gitlab'
+ - if @group.shared_projects.any?
+ .panel.panel-default
+ .panel-heading
+ Projects shared with #{@group.name}
+ %span.badge
+ #{@group.shared_projects.count}
+ %ul.well-list
+ - @group.shared_projects.sort_by(&:name).each do |project|
+ %li
+ %strong
+ = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ %span.label.label-gray
+ = repository_size(project)
+ %span.pull-right.light
+ %span.monospace= project.path_with_namespace + ".git"
+
.col-md-6
- if can?(current_user, :admin_group_member, @group)
.panel.panel-default
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index b120f4dea67..53b3cd04c68 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -37,8 +37,7 @@
- @hooks.each do |hook|
%li
.list-item-name
- = link_to admin_hook_path(hook) do
- %strong= hook.url
+ %strong= hook.url
%p SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
.pull-right
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index eaa94ed9e36..8c6b389bf15 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -12,6 +12,10 @@
.col-sm-10
= f.text_field :title, class: "form-control", required: true
.form-group
+ = f.label :description, class: 'control-label'
+ .col-sm-10
+ = f.text_field :description, class: "form-control js-quick-submit"
+ .form-group
= f.label :color, "Background color", class: 'control-label'
.col-sm-10
.input-group
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index e3ccbf6c3a8..5736a301910 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -1,5 +1,7 @@
%li{id: dom_id(label)}
- = render_colored_label(label)
- .pull-right
- = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
- = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
+ .label-row
+ = render_colored_label(label)
+ = markdown(label.description, pipeline: :single_line)
+ .pull-right
+ = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
+ = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index d67454c03e7..3c57e3dc174 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -1,5 +1,5 @@
- page_title "Labels"
-= link_to new_admin_label_path, class: "pull-right btn btn-new" do
+= link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do
New label
%h3.page-title
Labels
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 1484baa78e0..af9fdeb0734 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -1,12 +1,13 @@
- page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger]
-%ul.nav.nav-tabs.log-tabs
+%ul.nav-links.log-tabs
- loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
= link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab'
-%p.light To prevent performance issues admin logs output the last 2000 lines
+.gray-content-block
+ To prevent performance issues admin logs output the last 2000 lines
.tab-content
- loggers.each do |klass|
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index d9b481404f7..d39c0f44031 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -1,7 +1,7 @@
- page_title "Projects"
= render 'shared/show_aside'
-.row
+.row.prepend-top-default
%aside.col-md-3
.admin-filter
= form_tag admin_namespaces_projects_path, method: :get, class: '' do
@@ -47,10 +47,10 @@
.panel.panel-default
.panel-heading
Projects (#{@projects.total_count})
- .panel-head-actions
+ .controls
.dropdown.inline
%button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light sort:
+ %span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 5260eadf95b..d734e60682a 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,7 +1,7 @@
- page_title @project.name_with_namespace, "Projects"
%h3.page-title
Project: #{@project.name_with_namespace}
- = link_to edit_project_path(@project), class: "btn pull-right" do
+ = link_to edit_project_path(@project), class: "btn btn-nr pull-right" do
%i.fa.fa-pencil-square-o
Edit
%hr
@@ -38,7 +38,7 @@
%li
%span.light Created on:
%strong
- = @project.created_at.stamp("March 1, 1999")
+ = @project.created_at.to_s(:medium)
%li
%span.light http:
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
new file mode 100644
index 00000000000..8aea67f4497
--- /dev/null
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -0,0 +1,32 @@
+- user = spam_log.user
+%tr
+ %td
+ = time_ago_with_tooltip(spam_log.created_at)
+ %td
+ - if user
+ = link_to user.name, [:admin, user]
+ .light.small
+ Joined #{time_ago_with_tooltip(user.created_at)}
+ - else
+ (removed)
+ %td
+ = spam_log.source_ip
+ %td
+ = spam_log.via_api? ? 'Y' : 'N'
+ %td
+ = spam_log.noteable_type
+ %td
+ = spam_log.title
+ %td
+ = truncate(spam_log.description, length: 100)
+ %td
+ - if user
+ = link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true),
+ data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
+ %td
+ - if user && !user.blocked?
+ = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
+ - else
+ .btn.btn-xs.disabled
+ Already Blocked
+ = link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr"
diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml
new file mode 100644
index 00000000000..0fdd5bd9960
--- /dev/null
+++ b/app/views/admin/spam_logs/index.html.haml
@@ -0,0 +1,21 @@
+- page_title "Spam Logs"
+%h3.page-title Spam Logs
+%hr
+- if @spam_logs.present?
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th Date
+ %th User
+ %th Source IP
+ %th API?
+ %th Type
+ %th Title
+ %th Description
+ %th Primary Action
+ %th
+ = render @spam_logs
+ = paginate @spam_logs
+- else
+ %h4 There are no Spam Logs
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index e18dd9bc905..d2527ede995 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -58,9 +58,15 @@
= f.label :admin, class: 'control-label'
- if current_user == @user
.col-sm-10= f.check_box :admin, disabled: true
- .col-sm-10 You cannot remove your own admin rights
+ .col-sm-10 You cannot remove your own admin rights.
- else
.col-sm-10= f.check_box :admin
+
+ .form-group
+ = f.label :external, class: 'control-label'
+ .col-sm-10= f.check_box :external
+ .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups.
+
%fieldset
%legend Profile
.form-group
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 5e17b018163..ce5e21e54cc 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -7,12 +7,12 @@
.pull-right
- unless @user == current_user || @user.blocked?
- = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info"
- = link_to edit_admin_user_path(@user), class: "btn btn-grouped" do
+ = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
+ = link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
%i.fa.fa-pencil-square-o
Edit
%hr
-%ul.nav.nav-tabs
+%ul.nav-links
= nav_link(path: 'users#show') do
= link_to "Account", admin_user_path(@user)
= nav_link(path: 'users#groups') do
@@ -23,3 +23,4 @@
= link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do
= link_to "Identities", admin_user_identities_path(@user)
+.append-bottom-default
diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml
index 7d11edc79e2..6bc217f84cc 100644
--- a/app/views/admin/users/_profile.html.haml
+++ b/app/views/admin/users/_profile.html.haml
@@ -4,7 +4,7 @@
%ul.well-list
%li
%span.light Member since
- %strong= user.created_at.stamp("Aug 21, 2011")
+ %strong= user.created_at.to_s(:medium)
- unless user.public_email.blank?
%li
%span.light E-mail:
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index bc08458312c..0ee8dc962b9 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,101 +1,107 @@
- page_title "Users"
= render 'shared/show_aside'
-.row
- %aside.col-md-3
- .admin-filter
- %ul.nav.nav-pills.nav-stacked
- %li{class: "#{'active' unless params[:filter]}"}
- = link_to admin_users_path do
- Active
- %small.pull-right= User.active.count
- %li{class: "#{'active' if params[:filter] == "admins"}"}
- = link_to admin_users_path(filter: "admins") do
- Admins
- %small.pull-right= User.admins.count
- %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
- = link_to admin_users_path(filter: 'two_factor_enabled') do
- 2FA Enabled
- %small.pull-right= User.with_two_factor.count
- %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
- = link_to admin_users_path(filter: 'two_factor_disabled') do
- 2FA Disabled
- %small.pull-right= User.without_two_factor.count
- %li{class: "#{'active' if params[:filter] == "blocked"}"}
- = link_to admin_users_path(filter: "blocked") do
- Blocked
- %small.pull-right= User.blocked.count
- %li{class: "#{'active' if params[:filter] == "wop"}"}
- = link_to admin_users_path(filter: "wop") do
- Without projects
- %small.pull-right= User.without_projects.count
- %hr
- = form_tag admin_users_path, method: :get, class: 'form-inline' do
- .form-group
- = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
- = hidden_field_tag "filter", params[:filter]
- = button_tag class: 'btn btn-primary' do
- %i.fa.fa-search
- %hr
- = link_to 'Reset', admin_users_path, class: "btn btn-cancel"
+.admin-filter
+ %ul.nav-links
+ %li{class: "#{'active' unless params[:filter]}"}
+ = link_to admin_users_path do
+ Active
+ %small.badge= number_with_delimiter(User.active.count)
+ %li{class: "#{'active' if params[:filter] == "admins"}"}
+ = link_to admin_users_path(filter: "admins") do
+ Admins
+ %small.badge= number_with_delimiter(User.admins.count)
+ %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
+ = link_to admin_users_path(filter: 'two_factor_enabled') do
+ 2FA Enabled
+ %small.badge= number_with_delimiter(User.with_two_factor.count)
+ %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
+ = link_to admin_users_path(filter: 'two_factor_disabled') do
+ 2FA Disabled
+ %small.badge= number_with_delimiter(User.without_two_factor.count)
+ %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"}
+ = link_to admin_users_path(filter: 'external') do
+ External
+ %small.badge= number_with_delimiter(User.external.count)
+ %li{class: "#{'active' if params[:filter] == "blocked"}"}
+ = link_to admin_users_path(filter: "blocked") do
+ Blocked
+ %small.badge= number_with_delimiter(User.blocked.count)
+ %li{class: "#{'active' if params[:filter] == "wop"}"}
+ = link_to admin_users_path(filter: "wop") do
+ Without projects
+ %small.badge= number_with_delimiter(User.without_projects.count)
- %section.col-md-9
- .panel.panel-default
- .panel-heading
- Users (#{@users.total_count})
- .panel-head-actions
- .dropdown.inline
- %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"}
- %span.light sort:
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_name
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
- = sort_title_name
- = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
- = sort_title_recently_signin
- = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
- = sort_title_oldest_signin
- = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
- = sort_title_recently_created
- = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
- = sort_title_oldest_created
- = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
- = sort_title_recently_updated
- = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
- = sort_title_oldest_updated
-
- = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm"
- %ul.well-list
- - @users.each do |user|
+ .gray-content-block.second-block
+ .pull-right
+ .dropdown.inline
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ %span.light
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
+ = sort_title_name
+ %b.caret
+ %ul.dropdown-menu
%li
- .list-item-name
- - if user.blocked?
- %i.fa.fa-lock.cred
+ = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
+ = sort_title_name
+ = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
+ = sort_title_recently_signin
+ = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
+ = sort_title_oldest_signin
+ = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
+ = sort_title_recently_created
+ = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
+ = sort_title_oldest_created
+ = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
+ = sort_title_recently_updated
+ = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
+ = sort_title_oldest_updated
+
+ = link_to 'New User', new_admin_user_path, class: "btn btn-new"
+ = form_tag admin_users_path, method: :get, class: 'form-inline' do
+ .form-group
+ = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
+ = hidden_field_tag "filter", params[:filter]
+ = button_tag class: 'btn btn-primary' do
+ %i.fa.fa-search
+
+
+.panel.panel-default
+ %ul.well-list
+ - @users.each do |user|
+ %li
+ .list-item-name
+ - if user.blocked?
+ = icon("lock", class: "cred")
+ - else
+ = icon("user", class: "cgreen")
+ = link_to user.name, [:admin, user]
+ - if user.admin?
+ %strong.cred (Admin)
+ - if user.external?
+ %strong.cred (External)
+ - if user == current_user
+ %span.cred It's you!
+ .pull-right
+ %span.light
+ %i.fa.fa-envelope
+ = mail_to user.email, user.email, class: 'light'
+ &nbsp;
+ .pull-right
+ = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs'
+ - unless user == current_user
+ - if user.ldap_blocked?
+ = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do
+ %i.fa.fa-lock
+ Unblock
+ - elsif user.blocked?
+ = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success'
- else
- %i.fa.fa-user.cgreen
- = link_to user.name, [:admin, user]
- - if user.admin?
- %strong.cred (Admin)
- - if user == current_user
- %span.cred It's you!
- .pull-right
- %span.light
- %i.fa.fa-envelope
- = mail_to user.email, user.email, class: 'light'
- &nbsp;
- = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-xs"
- - unless user == current_user
- - if user.blocked?
- = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success"
- - else
- = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs btn-warning"
- - if user.access_locked?
- = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success", data: { confirm: 'Are you sure?' }
- - if user.can_be_removed?
- = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
- = paginate @users, theme: "gitlab"
+ = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning'
+ - if user.access_locked?
+ = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
+ - if user.can_be_removed?
+ = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove'
+= paginate @users, theme: "gitlab"
diff --git a/app/views/admin/users/keys.html.haml b/app/views/admin/users/keys.html.haml
index 07110717082..0f644121e62 100644
--- a/app/views/admin/users/keys.html.haml
+++ b/app/views/admin/users/keys.html.haml
@@ -1,3 +1,3 @@
-- page_title "Keys", @user.name, "Users"
+- page_title "SSH Keys", @user.name, "Users"
= render 'admin/users/head'
= render 'profiles/keys/key_table', admin: true
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 0848504b7a6..d37489bebea 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -48,6 +48,10 @@
Disabled
%li
+ %span.light External User:
+ %strong
+ = @user.external? ? "Yes" : "No"
+ %li
%span.light Can create groups:
%strong
= @user.can_create_group ? "Yes" : "No"
@@ -58,12 +62,12 @@
%li
%span.light Member since:
%strong
- = @user.created_at.stamp("Nov 12, 2031")
+ = @user.created_at.to_s(:medium)
- if @user.confirmed_at
%li
%span.light Confirmed at:
%strong
- = @user.confirmed_at.stamp("Nov 12, 2031")
+ = @user.confirmed_at.to_s(:medium)
- else
%li
%span.light Confirmed:
@@ -71,10 +75,26 @@
No
%li
+ %span.light Current sign-in IP:
+ %strong
+ - if @user.current_sign_in_ip
+ = @user.current_sign_in_ip
+ - else
+ never
+
+ %li
%span.light Current sign-in at:
%strong
- if @user.current_sign_in_at
- = @user.current_sign_in_at.stamp("Nov 12, 2031")
+ = @user.current_sign_in_at.to_s(:medium)
+ - else
+ never
+
+ %li
+ %span.light Last sign-in IP:
+ %strong
+ - if @user.last_sign_in_ip
+ = @user.last_sign_in_ip
- else
never
@@ -82,7 +102,7 @@
%span.light Last sign-in at:
%strong
- if @user.last_sign_in_at
- = @user.last_sign_in_at.stamp("Nov 12, 2031")
+ = @user.last_sign_in_at.to_s(:medium)
- else
never
diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml
deleted file mode 100644
index 11163813f3e..00000000000
--- a/app/views/ci/commits/_commit.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-%tr.build
- %td.status
- = ci_status_with_icon(commit.status)
- - if commit.running?
- &middot;
- = commit.stage
-
-
- %td.build-link
- = link_to ci_status_path(commit) do
- %strong #{commit.short_sha}
-
- %td.build-message
- %span= truncate_first_line(commit.git_commit_message)
-
- %td.build-branch
- - unless @ref
- %span
- - commit.refs.each do |ref|
- = link_to truncate(ref, length: 25), ci_project_path(@project, ref: ref)
-
- %td.duration
- - if commit.duration > 0
- #{time_interval_in_words commit.duration}
-
- %td.timestamp
- - if commit.finished_at
- %span #{time_ago_in_words commit.finished_at} ago
-
- - if commit.coverage
- %td.coverage
- #{commit.coverage}%
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index a144c43be47..0044d779c31 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -4,12 +4,12 @@
.row
= form_tag ci_lint_path, method: :post do
.form-group
- = label_tag :content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap'
+ = label_tag(:content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap')
.col-sm-12
- = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true
+ = text_area_tag(:content, @content, class: 'form-control span1', rows: 7, require: true)
.col-sm-12
.pull-left.prepend-top-10
- = submit_tag 'Validate', class: 'btn btn-success submit-yml'
+ = submit_tag('Validate', class: 'btn btn-success submit-yml')
.row.prepend-top-20
.col-sm-12
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index f98fd9f06ba..dc76599b776 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,9 +1,9 @@
.hidden-xs
= render "events/event_last_push", event: @last_push
-.gray-content-block
+.nav-block
- if current_user
- .pull-right
+ .controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
%i.fa.fa-rss
= render 'shared/event_filter'
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index 9f4be025bf2..b78e70ebc1e 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
%li{ class: ("active" unless params[:filter]) }
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 64bd356f546..3d17f74b709 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,7 +1,13 @@
-%ul.center-top-menu
- = nav_link(page: dashboard_groups_path) do
- = link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do
- Your Groups
- = nav_link(page: explore_groups_path) do
- = link_to explore_groups_path, title: 'Explore groups', data: {placement: 'bottom'} do
- Explore Groups
+.top-area
+ %ul.nav-links
+ = nav_link(page: dashboard_groups_path) do
+ = link_to dashboard_groups_path, title: 'Your groups' do
+ Your Groups
+ = nav_link(page: explore_groups_path) do
+ = link_to explore_groups_path, title: 'Explore groups' do
+ Explore Groups
+ - if current_user.can_create_group?
+ .nav-controls
+ = link_to new_group_path, class: "btn btn-new" do
+ = icon('plus')
+ New Group
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index f4a3e3162bf..9da3fcbd986 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,20 +1,22 @@
= content_for :flash_message do
= render 'shared/project_limit'
.top-area
- %ul.left-top-menu
+ %ul.nav-links
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
= nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do
Starred Projects
- = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path], html_options: { class: 'hidden-xs' }) do
+ = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
Explore Projects
- .projects-search-form
- = search_field_tag :filter_projects, nil, placeholder: 'Filter by name...', class: 'projects-list-filter form-control hidden-xs', spellcheck: false
+ .nav-controls
+ = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2"
+ = render 'shared/projects/dropdown'
- if current_user.can_create_project?
- = link_to new_project_path, class: 'btn btn-green' do
- %i.fa.fa-plus
+ = link_to new_project_path, class: 'btn btn-new' do
+ = icon('plus')
New Project
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index 0ae62d6f1b6..b25e8ea1f0c 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
Your Snippets
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index d5b7e729e7b..caca91af536 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -2,15 +2,6 @@
- header_title "Groups", dashboard_groups_path
= render 'dashboard/groups_head'
-.gray-content-block
- - if current_user.can_create_group?
- %span.pull-right.hidden-xs
- = link_to new_group_path, class: "btn btn-new" do
- %i.fa.fa-plus
- New Group
- .oneline
- Group members have access to all group projects.
-
%ul.content-list
- @group_members.each do |group_member|
- group = group_member.group
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index 07bda1c77f8..0d7b1b30dc3 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: issues_dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
- xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.any?
@issues.each do |issue|
issue_to_atom(xml, issue)
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 2d3da01178a..dfa5f80eef8 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -4,20 +4,15 @@
- if current_user
= auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues")
-.project-issuable-filter
- .controls
- .pull-left
- - if current_user
- .hidden-xs.pull-left
- = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
- %i.fa.fa-rss
-
+.top-area
+ = render 'shared/issuable/nav', type: :issues
+ .nav-controls
+ - if current_user
+ = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
+ = icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
- = render 'shared/issuable/filter', type: :issues
-
-.gray-content-block.second-block
- List all issues from all projects you have access to.
+= render 'shared/issuable/filter', type: :issues
.prepend-top-default
= render 'shared/issues'
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index c5a5ec21f78..fb016599fef 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -1,14 +1,12 @@
- page_title "Merge Requests"
- header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id)
-.project-issuable-filter
- .controls
+.top-area
+ = render 'shared/issuable/nav', type: :merge_requests
+ .nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
- = render 'shared/issuable/filter', type: :merge_requests
-
-.gray-content-block.second-block
- List all merge requests from all projects you have access to.
+= render 'shared/issuable/filter', type: :merge_requests
.prepend-top-default
= render 'shared/merge_requests'
diff --git a/app/views/dashboard/milestones/_issue.html.haml b/app/views/dashboard/milestones/_issue.html.haml
deleted file mode 100644
index 1408ebdd5dc..00000000000
--- a/app/views/dashboard/milestones/_issue.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
- %span.milestone-row
- - project = issue.project
- %strong #{project.name_with_namespace} &middot;
- = 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, 16), class: "avatar s16"
diff --git a/app/views/dashboard/milestones/_issues.html.haml b/app/views/dashboard/milestones/_issues.html.haml
deleted file mode 100644
index 9f350b772bd..00000000000
--- a/app/views/dashboard/milestones/_issues.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list issues-sortable-list" }
- - if issues
- - issues.each do |issue|
- = render 'issue', issue: issue
diff --git a/app/views/dashboard/milestones/_merge_request.html.haml b/app/views/dashboard/milestones/_merge_request.html.haml
deleted file mode 100644
index 77c46de030b..00000000000
--- a/app/views/dashboard/milestones/_merge_request.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid }
- %span.milestone-row
- - project = merge_request.project
- %strong #{project.name_with_namespace} &middot;
- = link_to [project.namespace.becomes(Namespace), project, merge_request] do
- %span.cgray ##{merge_request.iid}
- = 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, 16), class: "avatar s16"
diff --git a/app/views/dashboard/milestones/_merge_requests.html.haml b/app/views/dashboard/milestones/_merge_requests.html.haml
deleted file mode 100644
index 50057e2c636..00000000000
--- a/app/views/dashboard/milestones/_merge_requests.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list merge_requests-sortable-list" }
- - if merge_requests
- - merge_requests.each do |merge_request|
- = render 'merge_request', merge_request: merge_request
diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml
index 7c882a32702..6173ca6ab9b 100644
--- a/app/views/dashboard/milestones/_milestone.html.haml
+++ b/app/views/dashboard/milestones/_milestone.html.haml
@@ -1,25 +1,6 @@
-%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
- .row
- .col-sm-6
- %strong
- = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- .col-sm-6
- .pull-right.light #{milestone.percent_complete}% complete
- .row
- .col-sm-6
- = link_to issues_dashboard_path(milestone_title: milestone.title) do
- = pluralize milestone.issue_count, 'Issue'
- &middot;
- = link_to merge_requests_dashboard_path(milestone_title: milestone.title) do
- = pluralize milestone.merge_requests_count, 'Merge Request'
- .col-sm-6
- = milestone_progress_bar(milestone)
- .row
- .col-sm-6
- .expiration
- = render 'shared/milestone_expired', milestone: milestone
- .projects
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label.label-gray
- = milestone.project.name_with_namespace
+= render 'shared/milestones/milestone',
+ milestone_path: dashboard_milestone_path(milestone.safe_title, title: milestone.title),
+ issues_path: issues_dashboard_path(milestone_title: milestone.title),
+ merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title),
+ milestone: milestone,
+ dashboard: true
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index bec1692a4de..917bfbd47e9 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -1,14 +1,11 @@
- page_title "Milestones"
- header_title "Milestones", dashboard_milestones_path
-.project-issuable-filter
- .controls
- = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true
-
+.top-area
= render 'shared/milestones_filter'
-.gray-content-block
- List all milestones from all projects you have access to.
+ .nav-controls
+ = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true
.milestones
%ul.content-list
diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml
index 4316c358dcb..60c84a26420 100644
--- a/app/views/dashboard/milestones/show.html.haml
+++ b/app/views/dashboard/milestones/show.html.haml
@@ -1,105 +1,5 @@
-- page_title @milestone.title, "Milestones"
- header_title "Milestones", dashboard_milestones_path
-.detail-page-header
- .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" }
- - if @milestone.closed?
- Closed
- - else
- Open
- %span.identifier
- Milestone #{@milestone.title}
-
-.detail-page-description.gray-content-block.second-block
- %h2.title
- = markdown escape_once(@milestone.title), pipeline: :single_line
-
-- if @milestone.complete? && @milestone.active?
- .alert.alert-success.prepend-top-default
- %span All issues for this milestone are closed. You may close the milestone now.
-
-.table-holder
- %table.table
- %thead
- %tr
- %th Project
- %th Open issues
- %th State
- %th Due date
- - @milestone.milestones.each do |milestone|
- %tr
- %td
- = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
- %td
- = milestone.issues.opened.count
- %td
- - if milestone.closed?
- Closed
- - else
- Open
- %td
- = milestone.expires_at
-
-.context
- %p.lead
- Progress:
- #{@milestone.closed_items_count} closed
- &ndash;
- #{@milestone.open_items_count} open
- = milestone_progress_bar(@milestone)
-
-%ul.center-top-menu.no-top.no-bottom
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab' do
- Issues
- %span.badge= @milestone.issue_count
- %li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
- Merge Requests
- %span.badge= @milestone.merge_requests_count
- %li
- = link_to '#tab-participants', 'data-toggle' => 'tab' do
- Participants
- %span.badge= @milestone.participants.count
-
-.tab-content
- .tab-pane.active#tab-issues
- .gray-content-block.middle-block
- .pull-right
- = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All issues in this milestone
-
- .row.prepend-top-default
- .col-md-6
- = render 'issues', title: "Open", issues: @milestone.opened_issues
- .col-md-6
- = render 'issues', title: "Closed", issues: @milestone.closed_issues
-
- .tab-pane#tab-merge-requests
- .gray-content-block.middle-block
- .pull-right
- = link_to 'Browse Merge Requests', merge_requests_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All merge requests in this milestone
-
- .row.prepend-top-default
- .col-md-6
- = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
- .col-md-6
- = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
-
- .tab-pane#tab-participants
- .gray-content-block.middle-block
- .oneline
- All participants to this milestone
- %ul.bordered-list
- - @milestone.participants.each do |user|
- %li
- = link_to user, title: user.name, class: "darken" do
- = image_tag avatar_icon(user, 32), class: "avatar s32"
- %strong= truncate(user.name, lenght: 40)
- %br
- %small.cgray= user.username
+= render 'shared/milestones/top', milestone: @milestone
+= render 'shared/milestones/summary', milestone: @milestone
+= render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true
diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml
index cea9ffcc748..0ebd7c01bab 100644
--- a/app/views/dashboard/projects/_projects.html.haml
+++ b/app/views/dashboard/projects/_projects.html.haml
@@ -1,3 +1 @@
-.projects-list-holder
-
- = render 'shared/projects/list', projects: @projects, ci: true
+= render 'shared/projects/list', projects: @projects, ci: true
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index 4e7d6639727..d54c7cad7be 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,4 +1,4 @@
-- publicish_project_count = Project.publicish(current_user).count
+- publicish_project_count = ProjectsFinder.new.execute(current_user).count
%h3.page-title Welcome to GitLab!
%p.light Self hosted Git management application.
%hr
@@ -11,14 +11,14 @@
%br
- if current_user.can_create_project?
You can create up to
- %strong= pluralize(current_user.projects_limit, "project") + "."
+ %strong= pluralize(number_with_delimiter(current_user.projects_limit), "project") + "."
- else
If you are added to a project, it will be displayed here.
- if current_user.can_create_project?
.link_holder
= link_to new_project_path, class: "btn btn-new" do
- %i.fa.fa-plus
+ = icon('plus')
New Project
- if current_user.can_create_group?
@@ -44,7 +44,7 @@
.dashboard-intro-text
%p.slead
There are
- %strong= publicish_project_count
+ %strong= number_with_delimiter(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/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder
index c8c219f4cca..d4daf07c6c0 100644
--- a/app/views/dashboard/projects/index.atom.builder
+++ b/app/views/dashboard/projects/index.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
xml.id dashboard_projects_url
- xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events[0].updated_at.xmlschema if @events[0]
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 53abf274bdb..4565e752c1f 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -10,7 +10,7 @@
- if @last_push
= render "events/event_last_push", event: @last_push
-- if @projects.any?
+- if @projects.any? || params[:filter_projects]
= render 'projects'
- else
= render "zero_authorized_projects"
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index 07b6d57932e..d4e7862981c 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -3,32 +3,36 @@
= render 'dashboard/snippets_head'
-.gray-content-block
- .pull-right
+.nav-block
+ .controls
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
= icon('plus')
New Snippet
- .btn-group.btn-group-next.snippet-scope-menu
- = link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do
- All
- %span.badge
- = current_user.snippets.count
-
- = link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do
- Private
- %span.badge
- = current_user.snippets.are_private.count
-
- = link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do
- Internal
- %span.badge
- = current_user.snippets.are_internal.count
-
- = link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do
- Public
- %span.badge
- = current_user.snippets.are_public.count
+ .nav-links.snippet-scope-menu
+ %li{ class: ("active" unless params[:scope]) }
+ = link_to dashboard_snippets_path do
+ All
+ %span.badge
+ = current_user.snippets.count
+
+ %li{ class: ("active" if params[:scope] == "are_private") }
+ = link_to dashboard_snippets_path(scope: 'are_private') do
+ Private
+ %span.badge
+ = current_user.snippets.are_private.count
+
+ %li{ class: ("active" if params[:scope] == "are_internal") }
+ = link_to dashboard_snippets_path(scope: 'are_internal') do
+ Internal
+ %span.badge
+ = current_user.snippets.are_internal.count
+
+ %li{ class: ("active" if params[:scope] == "are_public") }
+ = link_to dashboard_snippets_path(scope: 'are_public') do
+ Public
+ %span.badge
+ = current_user.snippets.are_public.count
= render 'snippets/snippets'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
new file mode 100644
index 00000000000..4c848a50181
--- /dev/null
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -0,0 +1,26 @@
+%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) }
+ .todo-item.todo-block
+ = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
+
+ .todo-title
+ %span.author-name
+ - if todo.author
+ = link_to_author(todo)
+ - else
+ (removed)
+ %span.todo-label
+ = todo_action_name(todo)
+ = todo_target_link(todo)
+
+ &middot; #{time_ago_with_tooltip(todo.created_at)}
+
+ - if todo.pending?
+ .todo-actions.pull-right
+ = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
+ Done
+ = icon('spinner spin')
+
+ .todo-body
+ .todo-note
+ .md
+ = event_note(todo.body, project: todo.project)
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
new file mode 100644
index 00000000000..623381375a5
--- /dev/null
+++ b/app/views/dashboard/todos/index.html.haml
@@ -0,0 +1,66 @@
+- page_title "Todos"
+- header_title "Todos", dashboard_todos_path
+
+.top-area
+ %ul.nav-links
+ - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
+ %li{class: "todos-pending #{todo_pending_active}"}
+ = link_to todos_filter_path(state: 'pending') do
+ %span
+ To do
+ %span{class: 'badge'}
+ = todos_pending_count
+ - todo_done_active = ('active' if params[:state] == 'done')
+ %li{class: "todos-done #{todo_done_active}"}
+ = link_to todos_filter_path(state: 'done') do
+ %span
+ Done
+ %span{class: 'badge'}
+ = todos_done_count
+
+ .nav-controls
+ - if @todos.any?(&:pending?)
+ = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
+ Mark all as done
+ = icon('spinner spin')
+
+.todos-filters
+ .gray-content-block.second-block
+ = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
+ .filter-item.inline
+ = select_tag('project_id', todo_projects_options,
+ class: 'select2 trigger-submit', include_blank: true,
+ data: {placeholder: 'Project'})
+ .filter-item.inline
+ = users_select_tag(:author_id, selected: params[:author_id],
+ placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
+ .filter-item.inline
+ = select_tag('type', todo_types_options,
+ class: 'select2 trigger-submit', include_blank: true,
+ data: {placeholder: 'Type'})
+ .filter-item.inline.actions-filter
+ = select_tag('action_id', todo_actions_options,
+ class: 'select2 trigger-submit', include_blank: true,
+ data: {placeholder: 'Action'})
+
+.prepend-top-default
+ - if @todos.any?
+ - @todos.group_by(&:project).each do |group|
+ .panel.panel-default.panel-small.js-todos-list
+ - project = group[0]
+ .panel-heading
+ = link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
+
+ %ul.well-list.todos-list
+ = render group[1]
+ = paginate @todos, theme: "gitlab"
+ - else
+ .nothing-here-block You're all done!
+
+:javascript
+ new UsersSelect();
+
+ $('form.filter-form').on('submit', function (event) {
+ event.preventDefault();
+ Turbolinks.visit(this.action + '&' + $(this).serialize());
+ });
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index dbc8eda6196..d65fa60025c 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,10 +1,10 @@
- page_title "Sign in"
%div
- - if signin_enabled? || ldap_enabled?
+ - if signin_enabled? || ldap_enabled? || crowd_enabled?
= render 'devise/shared/signin_box'
-# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box
- - if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?
+ - if omniauth_enabled? && devise_mapping.omniauthable?
.clearfix.prepend-top-20
= render 'devise/shared/omniauth_box'
@@ -14,6 +14,6 @@
= render 'devise/shared/signup_box'
-# Show a message if none of the mechanisms above are enabled
- - if !signin_enabled? && !ldap_enabled? && !(Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?)
+ - if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
%div
No authentication methods configured.
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 41ad2c231d4..2c15e2c4891 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -7,7 +7,7 @@
%h3 Sign in
.login-body
- if form_based_providers.any?
- %ul.nav.nav-tabs
+ %ul.nav-links
- if crowd_enabled?
%li.active
= link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
diff --git a/app/views/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml
index 6a5c917049d..001a711b1dd 100644
--- a/app/views/doorkeeper/applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/applications/_delete_form.html.haml
@@ -1,4 +1,10 @@
- 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
+ - if defined? small
+ = button_tag type: "submit", class: "btn btn-transparent", data: { confirm: "Are you sure?" } do
+ %span.sr-only
+ Destroy
+ = icon('trash')
+ - else
+ = submit_tag 'Destroy', data: { confirm: "Are you sure?" }, class: submit_btn_css
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index 98a61ab211b..906b0676150 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for application, url: doorkeeper_submit_path(application), html: {class: 'form-horizontal', role: 'form'} do |f|
+= form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f|
- if application.errors.any?
.alert.alert-danger
%ul
@@ -6,25 +6,20 @@
%li= msg
.form-group
- = f.label :name, class: 'control-label'
-
- .col-sm-10
- = f.text_field :name, class: 'form-control', required: true
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: 'form-control', required: true
.form-group
- = f.label :redirect_uri, class: 'control-label'
-
- .col-sm-10
- = f.text_area :redirect_uri, class: 'form-control', required: true
+ = f.label :redirect_uri, class: 'label-light'
+ = f.text_area :redirect_uri, class: 'form-control', required: true
+ %span.help-block
+ Use one line per URI
+ - if Doorkeeper.configuration.native_redirect_uri
%span.help-block
- Use one line per URI
- - if Doorkeeper.configuration.native_redirect_uri
- %span.help-block
- Use
- %code= Doorkeeper.configuration.native_redirect_uri
- for local tests
+ Use
+ %code= Doorkeeper.configuration.native_redirect_uri
+ for local tests
- .form-actions
- = f.submit 'Submit', class: "btn btn-create"
- = link_to "Cancel", applications_profile_path, class: "btn btn-cancel"
+ .prepend-top-default
+ = f.submit 'Save application', class: "btn btn-create"
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index ba4c5b86efb..ea0b66c932b 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -1,19 +1,83 @@
- page_title "Applications"
-%h3.page-title Your applications
-%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
+- header_title page_title, applications_profile_path
-.table-holder
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Callback URL
- %th
- %th
- %tbody
- - @applications.each do |application|
- %tr{:id => "application_#{application.id}"}
- %td= link_to application.name, oauth_application_path(application)
- %td= application.redirect_uri
- %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link'
- %td= render 'delete_form', application: application
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ - if user_oauth_applications?
+ Manage applications that can use GitLab as an OAuth provider,
+ and applications that you've authorized to use your account.
+ - else
+ Manage applications that you've authorized to use your account.
+ .col-lg-9
+ - if user_oauth_applications?
+ %h5.prepend-top-0
+ Add new application
+ = render 'form', application: @application
+ %hr
+ - if user_oauth_applications?
+ .oauth-applications
+ %h5
+ Your applications (#{@applications.size})
+ - if @applications.any?
+ .table-responsive
+ %table.table
+ %thead
+ %tr
+ %th Name
+ %th Callback URL
+ %th Clients
+ %th.last-heading
+ %tbody
+ - @applications.each do |application|
+ %tr{id: "application_#{application.id}"}
+ %td= link_to application.name, oauth_application_path(application)
+ %td
+ - application.redirect_uri.split.each do |uri|
+ %div= uri
+ %td= application.access_tokens.count
+ %td
+ = link_to edit_oauth_application_path(application), class: "btn btn-transparent append-right-5" do
+ %span.sr-only
+ Edit
+ = icon('pencil')
+ = render 'delete_form', application: application, small: true
+ - else
+ .profile-settings-message.text-center
+ You don't have any applications
+ .oauth-authorized-applications.prepend-top-20.append-bottom-default
+ - if user_oauth_applications?
+ %h5
+ Authorized applications (#{@authorized_tokens.size})
+
+ - if @authorized_tokens.any?
+ .table-responsive
+ %table.table.table-striped
+ %thead
+ %tr
+ %th Name
+ %th Authorized At
+ %th Scope
+ %th
+ %tbody
+ - @authorized_apps.each do |app|
+ - token = app.authorized_tokens.order('created_at desc').first
+ %tr{id: "application_#{app.id}"}
+ %td= app.name
+ %td= token.created_at
+ %td= token.scopes
+ %td= render 'delete_form', application: app
+ - @authorized_anonymous_tokens.each do |token|
+ %tr
+ %td
+ Anonymous
+ %div.help-block
+ %em Authorization was granted by entering your username and password in the application.
+ %td= token.created_at
+ %td= token.scopes
+ %td= render 'delete_form', token: token
+ - else
+ .profile-settings-message.text-center
+ You don't have any authorized applications
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 15f9ee266c1..eae80e5210f 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -4,6 +4,15 @@
Authorize
%strong.text-info= @pre_auth.client.name
to use your account?
+
+ - if current_user.admin?
+ .text-warning.prepend-top-20
+ %p
+ = icon("exclamation-triangle fw")
+ You are an admin, which means granting access to
+ %strong #{@pre_auth.client.name}
+ will allow them to interact with GitLab as an admin as well. Proceed with caution.
+
- if @pre_auth.scopes
#oauth-permissions
%p This application will be able to:
@@ -25,4 +34,4 @@
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
- = submit_tag "Deny", class: "btn btn-danger prepend-left-10" \ No newline at end of file
+ = submit_tag "Deny", class: "btn btn-danger prepend-left-10"
diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml
new file mode 100644
index 00000000000..3443a8e2307
--- /dev/null
+++ b/app/views/emojis/index.html.haml
@@ -0,0 +1,11 @@
+.emoji-menu
+ .emoji-menu-content
+ = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
+ - AwardEmoji.emoji_by_category.each do |category, emojis|
+ %h5.emoji-menu-title
+ = AwardEmoji::CATEGORIES[category]
+ %ul.clearfix.emoji-menu-list
+ - emojis.each do |emoji|
+ %li.pull-left.text-center.emoji-menu-list-item
+ %button.emoji-menu-btn.text-center.js-emoji-btn{type: "button"}
+ = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml
index 4ba8b84fd92..dce4081288c 100644
--- a/app/views/events/_commit.html.haml
+++ b/app/views/events/_commit.html.haml
@@ -1,5 +1,5 @@
%li.commit
.commit-row-title
- = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: ''
+ = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '', title: truncate_sha(commit[:id])
&middot;
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 46432a92348..2d9d9dd6342 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,10 +1,10 @@
-- if event.proper?
+- if event.proper?(current_user)
.event-item{class: "#{event.body? ? "event-block" : "event-inline" }"}
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
- = cache [event, current_application_settings, "v2.1"] do
- = image_tag avatar_icon(event.author_email, 46), class: "avatar s46", alt:''
+ = cache [event, current_application_settings, "v2.2"] do
+ = image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:''
- if event.created_project?
= render "events/event/created_project", event: event
- elsif event.push?
diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml
index ffc37ad6178..5753158c24d 100644
--- a/app/views/events/_event_last_push.html.haml
+++ b/app/views/events/_event_last_push.html.haml
@@ -1,9 +1,9 @@
- if show_last_push_widget?(event)
- .gray-content-block.clear-block
+ .gray-content-block.clear-block.last-push-widget
.event-last-push
.event-last-push-text
%span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
+ = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.project.name) do
%strong= event.ref_name
%span at
%strong= link_to_project event.project
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index 4ecf1c33d2a..e9e16a7646f 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -4,7 +4,7 @@
= event_action_name(event)
- if event.target
- %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target]
+ %strong= link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target]
= event_preposition(event)
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 8bed5cdb9cc..235bd46107e 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -5,7 +5,7 @@
%strong= event.ref_name
- else
%strong
- = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name)
+ = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.target_title)
at
= link_to_project event.project
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index fcb07b04083..8ffca96bb4e 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -18,7 +18,7 @@
.pull-right
.dropdown.inline
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light sort:
+ %span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
diff --git a/app/views/explore/projects/_dropdown.html.haml b/app/views/explore/projects/_dropdown.html.haml
deleted file mode 100644
index b23a3c1e5c1..00000000000
--- a/app/views/explore/projects/_dropdown.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-.dropdown.inline
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light sort:
- - if @sort.present?
- = sort_options_hash[@sort]
- - elsif current_page?(trending_explore_projects_path) || current_page?(explore_root_path)
- Trending projects
- - elsif current_page?(starred_explore_projects_path)
- Most stars
- - else
- = sort_title_recently_created
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to trending_explore_projects_path do
- Trending projects
- = link_to starred_explore_projects_path do
- Most stars
- = 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/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 28b12c8dca8..cd485da5104 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,49 +1,40 @@
-.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", id: "projects_search", spellcheck: false
- .form-group
- = button_tag 'Search', class: "btn"
-
-.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
+- if current_user
+ .dropdown
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ = icon('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 filter_projects_path(visibility_level: nil) do
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)
+ - Gitlab::VisibilityLevel.values.each do |level|
+ %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' }
+ = link_to filter_projects_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
+- if @tags.present?
+ .dropdown
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ = icon('tags')
+ %span.light Tags:
+ - if params[:tag].present?
+ = params[:tag]
+ - else
+ Any
+ %b.caret
+ %ul.dropdown-menu
+ %li
+ = link_to filter_projects_path(tag: nil) do
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
- = render 'explore/projects/dropdown'
+ - @tags.each do |tag|
+ %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
+ = link_to filter_projects_path(tag: tag.name) do
+ = icon('tag')
+ = tag.name
diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml
new file mode 100644
index 00000000000..614b5431779
--- /dev/null
+++ b/app/views/explore/projects/_nav.html.haml
@@ -0,0 +1,10 @@
+%ul.nav-links
+ = nav_link(page: [trending_explore_projects_path, explore_root_path]) do
+ = link_to trending_explore_projects_path do
+ Trending
+ = nav_link(page: starred_explore_projects_path) do
+ = link_to starred_explore_projects_path do
+ Most stars
+ = nav_link(page: explore_projects_path) do
+ = link_to explore_projects_path do
+ All
diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml
index 669079e9521..708fbc27f55 100644
--- a/app/views/explore/projects/_projects.html.haml
+++ b/app/views/explore/projects/_projects.html.haml
@@ -1,6 +1 @@
-- if projects.any?
- .public-projects
- = render 'shared/projects/list', projects: projects
-- else
- .nothing-here-block
- No such projects
+= render 'shared/projects/list', projects: projects
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index b9a958fbe7b..42b50481b9d 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -6,7 +6,10 @@
- else
= render 'explore/head'
-.gray-content-block.clearfix.second-block
- = render 'filter'
+.top-area
+ = render 'explore/projects/nav'
+
+ .nav-controls
+ = render 'filter'
+
= render 'projects', projects: @projects
-= paginate @projects, theme: "gitlab"
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index 95d46e331f8..ec461755103 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -6,12 +6,5 @@
- else
= render 'explore/head'
-.explore-trending-block
- .gray-content-block.second-block
- .pull-right
- = render 'explore/projects/dropdown'
- .oneline
- %i.fa.fa-star
- See most starred projects
- = render 'projects', projects: @starred_projects
- = paginate @starred_projects, theme: 'gitlab'
+= render 'explore/projects/nav'
+= render 'projects', projects: @projects
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index fa0b718e48b..ec461755103 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -6,11 +6,5 @@
- else
= render 'explore/head'
-.explore-trending-block
- .gray-content-block.second-block
- .pull-right
- = render 'explore/projects/dropdown'
- .oneline
- %i.fa.fa-comments-o
- See most discussed projects for last month
- = render 'projects', projects: @trending_projects
+= render 'explore/projects/nav'
+= render 'projects', projects: @projects
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
new file mode 100644
index 00000000000..dc76599b776
--- /dev/null
+++ b/app/views/groups/_activities.html.haml
@@ -0,0 +1,12 @@
+.hidden-xs
+ = render "events/event_last_push", event: @last_push
+
+.nav-block
+ - if current_user
+ .controls
+ = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
+ %i.fa.fa-rss
+ = render 'shared/event_filter'
+
+.content_list
+= spinner
diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml
index bbafc08435a..cca7dc27b1c 100644
--- a/app/views/groups/_projects.html.haml
+++ b/app/views/groups/_projects.html.haml
@@ -1,11 +1 @@
-.projects-list-holder
- .projects-search-form
- .input-group
- = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
- - if can? current_user, :create_projects, @group
- %span.input-group-btn
- = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-green' do
- %i.fa.fa-plus
- New Project
-
- = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false, skip_namespace: true
+= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: true
diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml
new file mode 100644
index 00000000000..b1694c919d0
--- /dev/null
+++ b/app/views/groups/_shared_projects.html.haml
@@ -0,0 +1 @@
+= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: false
diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml
new file mode 100644
index 00000000000..f73e1d9e865
--- /dev/null
+++ b/app/views/groups/activity.html.haml
@@ -0,0 +1,9 @@
+= 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")
+
+- page_title "Activity"
+- header_title group_title(@group, "Activity", activity_group_path(@group))
+
+%section.activities
+ = render 'activities'
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 1dea77c2e96..83936d39b16 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,7 +1,6 @@
- header_title group_title(@group, "Settings", edit_group_path(@group))
-- @blank_container = true
-.panel.panel-default
+.panel.panel-default.prepend-top-default
.panel-heading
Group settings
.panel-body
@@ -26,12 +25,12 @@
.form-group
%hr
- = f.label :public, class: 'control-label' do
- Public
+ = f.label :share_with_group_lock, class: 'control-label' do
+ Share with group lock
.col-sm-10
.checkbox
- = f.check_box :public
- %span.descr Make this group public (even if there are no public projects inside this group)
+ = f.check_box :share_with_group_lock
+ %span.descr Prevent sharing a project with another group within this group
.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 a79a0fcdc8e..60234be8f83 100644
--- a/app/views/groups/group_members/_group_member.html.haml
+++ b/app/views/groups/group_members/_group_member.html.haml
@@ -1,5 +1,6 @@
- user = member.user
- return unless user || member.invite?
+- show_roles = local_assigns.fetch(:show_roles, true)
%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
%span{class: ("list-item-name" if show_controls)}
@@ -28,7 +29,7 @@
= link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
Resend invite
- - if should_user_see_group_roles?(current_user, @group)
+ - if show_roles && should_user_see_group_roles?(current_user, @group)
%span.pull-right
%strong.member-access-level= member.human_access
- if show_controls
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 3361d7e2a8d..e7ab4f2409b 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -4,7 +4,7 @@
.col-sm-10
= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
.help-block
- Search for existing users or invite new ones using their email address.
+ Search for users by name, username, or email, or invite new ones using their email address.
.form-group
= f.label :access_level, "Group Access", class: 'control-label'
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 335bf036074..6b7fd5746d6 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,8 +1,7 @@
- page_title "Members"
- header_title group_title(@group, "Members", group_group_members_path(@group))
-- @blank_container = true
-.group-members-page
+.group-members-page.prepend-top-default
- if current_user && current_user.can?(:admin_group_member, @group)
.panel.panel-default
.panel-heading
@@ -20,7 +19,7 @@
group members
%small
(#{@members.total_count})
- .pull-right
+ .controls
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index 66fe7e25871..486d1d8587a 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: issues_dashboard_url(format: :atom, private_token: @user.private_token), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
- xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.any?
@issues.each do |issue|
issue_to_atom(xml, issue)
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 90ade1e1680..b0805593fdc 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -4,17 +4,15 @@
- if current_user
= auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues")
-.project-issuable-filter
- .controls
- .pull-left
- - if current_user
- .hidden-xs.pull-left
- = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
- %i.fa.fa-rss
-
+.top-area
+ = render 'shared/issuable/nav', type: :issues
+ .nav-controls
+ - if current_user
+ = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
+ = icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
- = render 'shared/issuable/filter', type: :issues
+= render 'shared/issuable/filter', type: :issues
.gray-content-block.second-block
Only issues from
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index f662f5a8c17..e1c9dd931ee 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,11 +1,12 @@
- page_title "Merge Requests"
- header_title group_title(@group, "Merge Requests", merge_requests_group_path(@group))
-.project-issuable-filter
- .controls
+.top-area
+ = render 'shared/issuable/nav', type: :merge_requests
+ .nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
- = render 'shared/issuable/filter', type: :merge_requests
+= render 'shared/issuable/filter', type: :merge_requests
.gray-content-block.second-block
Only merge requests from
diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml
deleted file mode 100644
index 9b85d83d6d8..00000000000
--- a/app/views/groups/milestones/_issue.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
- %span.milestone-row
- - project = issue.project
- %strong #{project.name} &middot;
- = 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, 16), class: "avatar s16", alt: ''
diff --git a/app/views/groups/milestones/_issues.html.haml b/app/views/groups/milestones/_issues.html.haml
deleted file mode 100644
index 9f350b772bd..00000000000
--- a/app/views/groups/milestones/_issues.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list issues-sortable-list" }
- - if issues
- - issues.each do |issue|
- = render 'issue', issue: issue
diff --git a/app/views/groups/milestones/_merge_request.html.haml b/app/views/groups/milestones/_merge_request.html.haml
deleted file mode 100644
index e3aa4aad198..00000000000
--- a/app/views/groups/milestones/_merge_request.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid }
- %span.milestone-row
- - project = merge_request.project
- %strong #{project.name} &middot;
- = link_to [project.namespace.becomes(Namespace), project, merge_request] do
- %span.cgray ##{merge_request.iid}
- = 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, 16), class: "avatar s16", alt: ''
diff --git a/app/views/groups/milestones/_merge_requests.html.haml b/app/views/groups/milestones/_merge_requests.html.haml
deleted file mode 100644
index 50057e2c636..00000000000
--- a/app/views/groups/milestones/_merge_requests.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list merge_requests-sortable-list" }
- - if merge_requests
- - merge_requests.each do |merge_request|
- = render 'merge_request', merge_request: merge_request
diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml
index a20bf75bc39..4c4e0a26728 100644
--- a/app/views/groups/milestones/_milestone.html.haml
+++ b/app/views/groups/milestones/_milestone.html.haml
@@ -1,29 +1,5 @@
-%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
- .row
- .col-sm-6
- %strong
- = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title)
- .col-sm-6
- .pull-right.light #{milestone.percent_complete}% complete
- .row
- .col-sm-6
- = link_to issues_group_path(@group, milestone_title: milestone.title) do
- = pluralize milestone.issue_count, 'Issue'
- &middot;
- = link_to merge_requests_group_path(@group, milestone_title: milestone.title) do
- = pluralize milestone.merge_requests_count, 'Merge Request'
- .col-sm-6
- = milestone_progress_bar(milestone)
- .row
- .col-sm-6
- %div
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label.label-gray
- = milestone.project.name
- .col-sm-6
- - if can?(current_user, :admin_milestones, @group)
- - if milestone.closed?
- = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
- - else
- = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close"
+= render 'shared/milestones/milestone',
+ milestone_path: group_milestone_path(@group, milestone.safe_title, title: milestone.title),
+ issues_path: issues_group_path(@group, milestone_title: milestone.title),
+ merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title),
+ milestone: milestone
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index b221d3a89a4..ab307708b75 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -1,17 +1,15 @@
- page_title "Milestones"
- header_title group_title(@group, "Milestones", group_milestones_path(@group))
-.project-issuable-filter
- .controls
- - if can?(current_user, :admin_milestones, @group)
- .pull-right
- %span.pull-right.hidden-xs
- = link_to new_group_milestone_path(@group), class: "btn btn-new" do
- = icon('plus')
- New Milestone
-
+.top-area
= render 'shared/milestones_filter'
+ .nav-controls
+ - if can?(current_user, :admin_milestones, @group)
+ = link_to new_group_milestone_path(@group), class: "btn btn-new" do
+ = icon('plus')
+ New Milestone
+
.gray-content-block
Only milestones from
%strong #{@group.name}
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index 3894a0ece74..a8e1ed77da9 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -8,18 +8,18 @@
This will create milestone in every selected project
%hr
-= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-requires-input' } do |f|
+= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f|
.row
.col-md-6
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
- = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true
+ = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
- = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
+ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
.clearfix
.error-alert
.form-group
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index d063b257b5e..fb6f0da28f8 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,112 +1,4 @@
-- page_title @milestone.title, "Milestones"
= render "header_title"
-
-.detail-page-header
- .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" }
- - if @milestone.closed?
- Closed
- - else
- Open
- %span.identifier
- Milestone #{@milestone.title}
- .pull-right
- - if can?(current_user, :admin_milestones, @group)
- - if @milestone.active?
- = link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close"
- - else
- = link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
-
-.detail-page-description.gray-content-block.second-block
- %h2.title
- = markdown escape_once(@milestone.title), pipeline: :single_line
-
-- if @milestone.complete? && @milestone.active?
- .alert.alert-success.prepend-top-default
- %span All issues for this milestone are closed. You may close the milestone now.
-
-.table-holder
- %table.table
- %thead
- %tr
- %th Project
- %th Open issues
- %th State
- %th Due date
- - @milestone.milestones.each do |milestone|
- %tr
- %td
- = link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
- %td
- = milestone.issues.opened.count
- %td
- - if milestone.closed?
- Closed
- - else
- Open
- %td
- = milestone.expires_at
-
-.context
- %p.lead
- Progress:
- #{@milestone.closed_items_count} closed
- &ndash;
- #{@milestone.open_items_count} open
- = milestone_progress_bar(@milestone)
-
-%ul.center-top-menu.no-top.no-bottom
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab' do
- Issues
- %span.badge= @milestone.issue_count
- %li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
- Merge Requests
- %span.badge= @milestone.merge_requests_count
- %li
- = link_to '#tab-participants', 'data-toggle' => 'tab' do
- Participants
- %span.badge= @milestone.participants.count
-
-.tab-content
- .tab-pane.active#tab-issues
- .gray-content-block.middle-block
- .pull-right
- = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All issues in this milestone
-
- .row.prepend-top-default
- .col-md-6
- = render 'issues', title: "Open", issues: @milestone.opened_issues
- .col-md-6
- = render 'issues', title: "Closed", issues: @milestone.closed_issues
-
- .tab-pane#tab-merge-requests
- .gray-content-block.middle-block
- .pull-right
- = link_to 'Browse Merge Requests', merge_requests_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All merge requests in this milestone
-
- .row.prepend-top-default
- .col-md-6
- = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
- .col-md-6
- = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
-
- .tab-pane#tab-participants
- .gray-content-block.middle-block
- .oneline
- All participants to this milestone
-
- %ul.bordered-list
- - @milestone.participants.each do |user|
- %li
- = link_to user, title: user.name, class: "darken" do
- = image_tag avatar_icon(user, 32), class: "avatar s32"
- %strong= truncate(user.name, lenght: 40)
- %br
- %small.cgray= user.username
+= render 'shared/milestones/top', milestone: @milestone, group: @group
+= render 'shared/milestones/summary', milestone: @milestone
+= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index f1d507a50c7..dd75766121e 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,14 +1,14 @@
- page_title "Projects"
- header_title group_title(@group, "Projects", projects_group_path(@group))
-.panel.panel-default
+.panel.panel-default.prepend-top-default
.panel-heading
%strong= @group.name
projects:
- if can? current_user, :admin_group, @group
- .panel-head-actions
+ .controls
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
- %i.fa.fa-plus
+ = icon('plus')
New Project
%ul.well-list
- @projects.each do |project|
diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder
index 7ea574434c3..c66b82bb484 100644
--- a/app/views/groups/show.atom.builder
+++ b/app/views/groups/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: group_url(@group), rel: "alternate", type: "text/html"
xml.id group_url(@group)
- xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events[0].updated_at.xmlschema if @events[0]
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index c2c7c581b3e..23a34ac36dd 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
- unless can?(current_user, :read_group, @group)
- @disable_search_panel = true
@@ -6,6 +8,12 @@
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
.cover-block
+ .cover-controls
+ - if @group && can?(current_user, :admin_group, @group)
+ = link_to icon('pencil'), edit_group_path(@group), class: 'btn'
+ - if current_user
+ = link_to icon('rss'), group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn'
+
.avatar-holder
= link_to group_icon(@group), target: '_blank' do
= image_tag group_icon(@group), class: "avatar group-avatar s90"
@@ -20,32 +28,33 @@
= markdown(@group.description, pipeline: :description)
- if can?(current_user, :read_group, @group)
- %ul.center-top-menu.no-top
- %li.active
- = link_to "#activity", 'data-toggle' => 'tab' do
- Activity
- - if @projects.present?
- %li
- = link_to "#projects", 'data-toggle' => 'tab' do
- Projects
-
- .tab-content
- .tab-pane.active#activity
- .gray-content-block.activity-filter-block
- - if current_user
- = render "events/event_last_push", event: @last_push
- .pull-right
- = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' do
- %i.fa.fa-rss
-
- = render 'shared/event_filter'
-
- .content_list
- = spinner
-
- .tab-pane#projects
- = render "projects", projects: @projects
+ %div{ class: container_class }
+ .top-area
+ %ul.nav-links
+ %li.active
+ = link_to "#projects", 'data-toggle' => 'tab' do
+ All Projects
+ - if @shared_projects.present?
+ %li
+ = link_to "#shared", 'data-toggle' => 'tab' do
+ Shared Projects
+ .nav-controls
+ = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
+ = render 'shared/projects/dropdown'
+ - if can? current_user, :create_projects, @group
+ = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
+ = icon('plus')
+ New Project
+
+ .tab-content
+ .tab-pane.active#projects
+ = render "projects", projects: @projects
+
+ - if @shared_projects.present?
+ .tab-pane#shared
+ = render "shared_projects", projects: @shared_projects
- else
- %p
- This group does not have public projects
+ %p.nav-links.no-top
+ No projects to show
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index e8e331dd109..da3c3711cdd 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -22,6 +22,14 @@
%td.shortcut
.key ?
%td Show this dialog
+ %tr
+ %td.shortcut
+ - if browser.mac?
+ .key &#8984; shift p
+ - else
+ .key ctrl shift p
+
+ %td Toggle Markdown preview
%tbody
%tr
%th
@@ -40,6 +48,28 @@
%td.shortcut
.key enter
%td Open Selection
+ %tbody
+ %tr
+ %th
+ %th Finding Project File
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ %td Move selection up
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-down
+ %td Move selection down
+ %tr
+ %td.shortcut
+ .key enter
+ %td Open Selection
+ %tr
+ %td.shortcut
+ .key esc
+ %td Go back
.col-lg-4
%table.shortcut-mappings
@@ -135,6 +165,10 @@
.key s
%td
Go to snippets
+ %tr
+ %td.shortcut
+ .key t
+ %td Go to finding file
.col-lg-4
%table.shortcut-mappings
%tbody{ class: 'hidden-shortcut network', style: 'display:none' }
@@ -203,6 +237,10 @@
%td.shortcut
.key r
%td Reply (quoting selected text)
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit issue
%tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
%tr
%th
@@ -219,3 +257,7 @@
%td.shortcut
.key r
%td Reply (quoting selected text)
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit merge request
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index d9ffda884c8..d084559abc3 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -19,6 +19,8 @@
%li
= link_to 'Buttons', '#buttons'
%li
+ = link_to 'Dropdowns', '#dropdowns'
+ %li
= link_to 'Panels', '#panels'
%li
= link_to 'Alerts', '#alerts'
@@ -31,64 +33,91 @@
%h2#blocks Blocks
- %h4
+ .lead
+ Content block separated with botton border
+ %code .content-block
+
+ .example
+ .content-block
+ %h4 Normal block inside content
+ = lorem
+
+ .content-block
+ %h4 Second block
+ = lorem
+
+ .lead
+ Gray content block with side padding using
%code .gray-content-block
- .gray-content-block.middle-block
- %h4 Normal block inside content
- = lorem
+ .example
+ .gray-content-block
+ %h4 Normal block inside content
+ = lorem
- .gray-content-block.second-block
- %h4 Second block
- = lorem
+ .gray-content-block.second-block
+ %h4 Second block
+ = lorem
- %h4
+ .lead
+ Cover block for profile page with avatar, name and description
%code .cover-block
- %br
- .cover-block
- .avatar-holder
- = image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
- .cover-title
- John Smith
-
- .cover-desc
- = lorem
+ .example
+ .cover-block
+ .avatar-holder
+ = image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
+ .cover-title
+ John Smith
- .cover-controls
- = link_to '#', class: 'btn btn-gray' do
- = icon('pencil')
- &nbsp;
- = link_to '#', class: 'btn btn-gray' do
- = icon('rss')
+ .cover-desc
+ = lorem
+
+ .cover-controls
+ = link_to '#', class: 'btn btn-gray' do
+ = icon('pencil')
+ &nbsp;
+ = link_to '#', class: 'btn btn-gray' do
+ = icon('rss')
%h2#lists Lists
- %h4
+ .lead
+ Simple list using
%code .content-list
- %ul.content-list
- %li
- One item
- %li
- One item
- %li
- One item
- %h4
- %code .well-list
- %ul.well-list
- %li
- One item
- %li
- One item
- %li
- One item
+ .example
+ %ul.content-list
+ %li
+ One item
+ %li
+ One item
+ %li
+ One item
- %h4
- %code .panel .well-list
+ .lead
+ List with avatar, title and description using
+ %code .content-list
+
+ .example
+ %ul.content-list
+ %li
+ = image_tag 'no_avatar.png', class: 'avatar s40'
+ .title Title
+ .description Description
+ %li
+ = image_tag 'no_avatar.png', class: 'avatar s40'
+ .title Title
+ .description Description
+ %li
+ = image_tag 'no_avatar.png', class: 'avatar s40'
+ .title Title
+ .description Description
- .panel.panel-default
- .panel-heading Your list
+ .lead
+ List with hover effect
+ %code .well-list
+ .example
%ul.well-list
%li
One item
@@ -97,17 +126,18 @@
%li
One item
- %h4
- %code .bordered-list
- %ul.bordered-list
- %li
- One item
- %li
- One item
- %li
- One item
-
-
+ .lead
+ List inside panel
+ .example
+ .panel.panel-default
+ .panel-heading Your list
+ %ul.well-list
+ %li
+ One item
+ %li
+ One item
+ %li
+ One item
%h2#tables Tables
@@ -138,27 +168,34 @@
%h2#navs Navigation
- %h4
- %code .center-top-menu
- .example
- %ul.center-top-menu
- %li.active
- %a Open
- %li
- %a Closed
+ .lead
+ Holder for top page navigation. Includes navigation, search field, sorting and button
+ %code .top-area
- %h4
- %code .btn-group.btn-group-next
.example
- %div.btn-group.btn-group-next
- %a.btn.active Open
- %a.btn Closed
-
-
- %h4
- %code .nav.nav-tabs
+ .top-area
+ %ul.nav-links
+ %li.active
+ %a Open
+ %li
+ %a Closed
+ .nav-controls
+ = text_field_tag 'sample', nil, class: 'form-control'
+ .dropdown
+ %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %span Sort by name
+ = icon('chevron-down')
+ %ul.dropdown-menu
+ %li
+ %a Sort by date
+
+ = link_to 'New issue', '#', class: 'btn btn-new'
+
+ .lead
+ Only nav links without button and search
+ %code .nav-links
.example
- %ul.nav.nav-tabs
+ %ul.nav-links
%li.active
%a Open
%li
@@ -177,6 +214,227 @@
%button.btn.btn-danger{:type => "button"} Danger
%button.btn.btn-link{:type => "button"} Link
+ %h2#dropdowns Dropdowns
+
+ .example
+ .clearfix
+ .dropdown.inline.pull-left
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ %ul.dropdown-menu
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .dropdown.inline.pull-right
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-selectable
+ %li
+ %a.is-active{href: "#"}
+ Dropdown Option
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.is-active{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li.divider
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .dropdown-footer
+ %strong Tip:
+ If an author is not a member of this project, you can still filter by his name while using the search field.
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown loading
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading
+ .dropdown-title
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.is-active{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li.divider
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .dropdown-footer
+ %strong Tip:
+ If an author is not a member of this project, you can still filter by his name while using the search field.
+ .dropdown-loading
+ = icon('spinner spin')
+
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown user
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user
+ .dropdown-title
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.dropdown-menu-user-link.is-active{href: "#"}
+ = link_to_member_avatar(current_user, size: 30)
+ %strong.dropdown-menu-user-full-name
+ = current_user.name
+ .dropdown-menu-user-username
+ = current_user.to_reference
+
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown page 2
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two
+ .dropdown-page-one
+ .dropdown-title
+ %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
+ = icon('arrow-left')
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.dropdown-menu-user-link.is-active{href: "#"}
+ = link_to_member_avatar(current_user, size: 30)
+ %strong.dropdown-menu-user-full-name
+ = current_user.name
+ .dropdown-menu-user-username
+ = current_user.to_reference
+ .dropdown-page-two
+ .dropdown-title
+ %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
+ = icon('arrow-left')
+ %span Create label
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Name new label"}
+ .dropdown-content
+ %button.btn.btn-primary
+ Create
+
+ .example
+ %div
+ .dropdown.inline
+ %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Projects
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Go to project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ .dropdown-loading
+ = icon('spinner spin')
+ :javascript
+ $('#js-project-dropdown').glDropdown({
+ data: function (term, callback) {
+ Api.projects(term, "last_activity_at", function (data) {
+ callback(data);
+ });
+ },
+ text: function (project) {
+ return project.name_with_namespace || project.name;
+ },
+ selectable: true,
+ fieldName: "author_id",
+ filterable: true,
+ search: {
+ fields: ['name_with_namespace']
+ },
+ id: function (data) {
+ return data.id;
+ },
+ isSelected: function (data) {
+ return data.id === 2;
+ }
+ })
+
+ .example
+ %div
+ = dropdown_tag("Projects", options: { title: "Go to project", filter: true, placeholder: "Filter projects" })
+
%h2#panels Panels
.row
@@ -221,43 +479,47 @@
%h2#forms Forms
- %h4
+ .lead
+ Horizontal form when label rendered inline with input
%code form.horizontal-form
- %form.form-horizontal
- .form-group
- %label.col-sm-2.control-label{:for => "inputEmail3"} Email
- .col-sm-10
- %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/
- .form-group
- %label.col-sm-2.control-label{:for => "inputPassword3"} Password
- .col-sm-10
- %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- %label
- %input{:type => "checkbox"}/
- Remember me
- .form-group
- .col-sm-offset-2.col-sm-10
- %button.btn.btn-default{:type => "submit"} Sign in
-
- %h4
+ .example
+ %form.form-horizontal
+ .form-group
+ %label.col-sm-2.control-label{:for => "inputEmail3"} Email
+ .col-sm-10
+ %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/
+ .form-group
+ %label.col-sm-2.control-label{:for => "inputPassword3"} Password
+ .col-sm-10
+ %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ %label
+ %input{:type => "checkbox"}/
+ Remember me
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ %button.btn.btn-default{:type => "submit"} Sign in
+
+ .lead
+ Form when label rendered above input
%code form
- %form
- .form-group
- %label{:for => "exampleInputEmail1"} Email address
- %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/
- .form-group
- %label{:for => "exampleInputPassword1"} Password
- %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/
- .checkbox
- %label
- %input{:type => "checkbox"}/
- Remember me
- %button.btn.btn-default{:type => "submit"} Sign in
+ .example
+ %form
+ .form-group
+ %label{:for => "exampleInputEmail1"} Email address
+ %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/
+ .form-group
+ %label{:for => "exampleInputPassword1"} Password
+ %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/
+ .checkbox
+ %label
+ %input{:type => "checkbox"}/
+ Remember me
+ %button.btn.btn-default{:type => "submit"} Sign in
%h2#file File
%h4
diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml
index 00c5f0b6f4e..c805914fc3f 100644
--- a/app/views/kaminari/gitlab/_next_page.html.haml
+++ b/app/views/kaminari/gitlab/_next_page.html.haml
@@ -5,5 +5,9 @@
-# num_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li.next
- = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, rel: 'next', remote: remote
+- if current_page.last?
+ %li{ class: "next disabled" }
+ %span= raw(t 'views.pagination.next')
+- else
+ %li{ class: "next" }
+ = link_to raw(t 'views.pagination.next'), url, rel: 'next', remote: remote
diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml
index 2f645186921..a12c53bcfe7 100644
--- a/app/views/kaminari/gitlab/_paginator.html.haml
+++ b/app/views/kaminari/gitlab/_paginator.html.haml
@@ -10,13 +10,13 @@
%ul.pagination.clearfix
- unless current_page.first?
= first_page_tag unless num_pages < 5 # As kaminari will always show the first 5 pages
- = prev_page_tag
+ = prev_page_tag
- each_page do |page|
- if page.left_outer? || page.right_outer? || page.inside_window?
= page_tag page
- elsif !page.was_truncated?
= gap_tag
+ = next_page_tag
- unless current_page.last?
- = next_page_tag
= last_page_tag unless num_pages < 5
diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml
index f673abdb3ae..afb20455e0a 100644
--- a/app/views/kaminari/gitlab/_prev_page.html.haml
+++ b/app/views/kaminari/gitlab/_prev_page.html.haml
@@ -5,5 +5,9 @@
-# num_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li{class: "prev" }
- = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote
+- if current_page.first?
+ %li{ class: "prev disabled" }
+ %span= raw(t 'views.pagination.previous')
+- else
+ %li{ class: "prev" }
+ = link_to raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote
diff --git a/app/views/layouts/_broadcast.html.haml b/app/views/layouts/_broadcast.html.haml
index e7d477c225e..3a7e0929c16 100644
--- a/app/views/layouts/_broadcast.html.haml
+++ b/app/views/layouts/_broadcast.html.haml
@@ -1,4 +1 @@
-- if broadcast_message.present?
- .broadcast-message{ style: broadcast_styling(broadcast_message) }
- %i.fa.fa-bullhorn
- = broadcast_message.message
+= broadcast_message
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index dd133ee8b56..79cdbac1f37 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -1,10 +1,13 @@
+- page_description brand_title unless page_description
+
+- site_name = "GitLab"
%head{prefix: "og: http://ogp.me/ns#"}
%meta{charset: "utf-8"}
%meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
-# Open Graph - http://ogp.me/
%meta{property: 'og:type', content: "object"}
- %meta{property: 'og:site_name', content: "GitLab"}
+ %meta{property: 'og:site_name', content: site_name}
%meta{property: 'og:title', content: page_title}
%meta{property: 'og:description', content: page_description}
%meta{property: 'og:image', content: page_image}
@@ -17,7 +20,7 @@
%meta{property: 'twitter:image', content: page_image}
= page_card_meta_tags
- %title= page_title('GitLab')
+ %title= page_title(site_name)
%meta{name: "description", content: page_description}
= favicon_link_tag 'favicon.ico'
@@ -41,6 +44,7 @@
= favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76'
= favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120'
= favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152'
+ %link{rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)'}
-# Windows 8 pinned site tile
%meta{name: 'msapplication-TileImage', content: image_path('msapplication-tile.png')}
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 035fe0056d3..96b38485425 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -1,4 +1,6 @@
- project = @target_project || @project
-:javascript
- GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: @noteable.class, type_id: params[:id])}"
- GitLab.GfmAutoComplete.setup();
+
+- if @noteable
+ :javascript
+ GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: @noteable.class, type_id: params[:id])}"
+ GitLab.GfmAutoComplete.setup();
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index ec7cd79bc54..c799e9c588d 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,9 +1,10 @@
-.page-with-sidebar{ class: page_sidebar_class }
+.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
= render "layouts/broadcast"
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do
+ %a#logo
= brand_header_logo
+ = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do
.gitlab-text-container
%h3 GitLab
@@ -24,7 +25,7 @@
.content-wrapper
= render "layouts/flash"
= yield :flash_message
- %div{ class: container_class }
+ %div{ class: (container_class unless @no_container) }
.content
.clearfix
= yield
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index a44f5762a6b..54af2c3063c 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -1,6 +1,6 @@
.search
= form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f|
- = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input form-control", spellcheck: false
+ = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1"
= hidden_field_tag :group_id, @group.try(:id)
- if @project && @project.persisted?
= hidden_field_tag :project_id, @project.id
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 678ed3c2c1f..babfb032236 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -5,11 +5,7 @@
-# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
= yield :scripts_body_top
- - if current_user
- = render "layouts/header/default", title: header_title
- - else
- = render "layouts/header/public", title: header_title
-
+ = render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar
= yield :scripts_body
diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml
index 7e90af21b21..a13241bebee 100644
--- a/app/views/layouts/ci/_page.html.haml
+++ b/app/views/layouts/ci/_page.html.haml
@@ -2,8 +2,9 @@
= render "layouts/broadcast"
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do
+ %a#logo
= brand_header_logo
+ = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do
.gitlab-text-container
%h3 GitLab
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index 31888c5580e..2e483b7148d 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -1,5 +1,6 @@
-- page_title @group.name
-- header_title group_title(@group) unless header_title
-- sidebar "group" unless sidebar
+- page_title @group.name
+- page_description @group.description unless page_description
+- header_title group_title(@group) unless header_title
+- sidebar "group" unless sidebar
= render template: "layouts/application"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 3892ef8eefa..77d01a7736c 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -13,27 +13,39 @@
%li.visible-sm.visible-xs
= link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
- - if session[:impersonator_id]
- %li.impersonation
- = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
- = icon('user-secret fw')
- - if current_user.is_admin?
+ - if current_user
+ - if session[:impersonator_id]
+ %li.impersonation
+ = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = icon('user-secret fw')
+ - if current_user.is_admin?
+ %li
+ = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('wrench fw')
%li
- = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('wrench fw')
- - if current_user.can_create_project?
+ = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ %span.badge.todos-pending-count
+ = todos_pending_count
+ - if current_user.can_create_project?
+ %li
+ = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('plus fw')
+ - if Gitlab::Sherlock.enabled?
+ %li
+ = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
+ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('tachometer fw')
%li
- = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('plus fw')
- - if Gitlab::Sherlock.enabled?
- %li
- = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
- data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('tachometer fw')
- %li
- = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('sign-out')
+ = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('sign-out')
+ - else
+ .pull-right
+ = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
+
%h1.title= title
= render 'shared/outdated_browser'
+- if @project && !@project.empty_repo?
+ :javascript
+ var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}";
diff --git a/app/views/layouts/header/_public.html.haml b/app/views/layouts/header/_public.html.haml
deleted file mode 100644
index a6a26518a0e..00000000000
--- a/app/views/layouts/header/_public.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
- %div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
- .header-content
- - unless current_controller?('sessions')
- .pull-right
- = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
-
- %h1.title= title
-
-= render 'shared/outdated_browser'
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index c60ac5eefac..280a1b93729 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -1,6 +1,6 @@
%ul.nav.nav-sidebar
= nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: "Stats" do
+ = link_to admin_root_path, title: 'Overview' do
= icon('dashboard fw')
%span
Overview
@@ -25,17 +25,17 @@
%span
Deploy Keys
= nav_link path: ['runners#index', 'runners#show'] do
- = link_to admin_runners_path do
+ = link_to admin_runners_path, title: 'Runners' do
= icon('cog fw')
%span
Runners
- %span.count= Ci::Runner.count(:all)
+ %span.count= number_with_delimiter(Ci::Runner.count(:all))
= nav_link path: 'builds#index' do
- = link_to admin_builds_path do
+ = link_to admin_builds_path, title: 'Builds' do
= icon('link fw')
%span
Builds
- %span.count= Ci::Build.count(:all)
+ %span.count= number_with_delimiter(Ci::Build.count(:all))
= nav_link(controller: :logs) do
= link_to admin_logs_path, title: 'Logs' do
= icon('file-text fw')
@@ -56,6 +56,11 @@
= icon('cog fw')
%span
Background Jobs
+ = nav_link(controller: :appearances) do
+ = link_to admin_appearances_path, title: 'Appearances' do
+ = icon('image')
+ %span
+ Appearance
= nav_link(controller: :applications) do
= link_to admin_applications_path, title: 'Applications' do
@@ -80,7 +85,15 @@
= icon('exclamation-circle fw')
%span
Abuse Reports
- %span.count= AbuseReport.count(:all)
+ %span.count= number_with_delimiter(AbuseReport.count(:all))
+
+ - if askimet_enabled?
+ = nav_link(controller: :spam_logs) do
+ = link_to admin_spam_logs_path, title: "Spam Logs" do
+ = icon('exclamation-triangle fw')
+ %span
+ Spam Logs
+ %span.count= number_with_delimiter(SpamLog.count(:all))
= nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
= link_to admin_application_settings_path, title: 'Settings' do
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index da698831300..db0cf393922 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -4,6 +4,12 @@
= icon('home fw')
%span
Projects
+ = nav_link(controller: :todos) do
+ = link_to dashboard_todos_path, title: 'Todos' do
+ = icon('bell fw')
+ %span
+ Todos
+ %span.count= number_with_delimiter(todos_pending_count)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do
= icon('dashboard fw')
@@ -24,13 +30,13 @@
= 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
- = icon('tasks fw')
- %span
- Merge Requests
- %span.count= current_user.assigned_merge_requests.opened.count
+ %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
+ = nav_link(path: 'dashboard#merge_requests') do
+ = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do
+ = icon('tasks fw')
+ %span
+ Merge Requests
+ %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
= nav_link(controller: :snippets) do
= link_to dashboard_snippets_path, title: 'Snippets' do
= icon('clipboard fw')
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 68da8d5de2a..59411ae1da1 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -9,10 +9,15 @@
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do
- = icon('dashboard fw')
+ = icon('group fw')
%span
Group
- if can?(current_user, :read_group, @group)
+ = nav_link(path: 'groups#activity') do
+ = link_to activity_group_path(@group), title: 'Activity' do
+ = icon('dashboard fw')
+ %span
+ Activity
- if current_user
= nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do
@@ -25,14 +30,14 @@
%span
Issues
- if current_user
- %span.count= Issue.opened.of_group(@group).count
+ %span.count= number_with_delimiter(Issue.opened.of_group(@group).count)
= nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
= icon('tasks fw')
%span
Merge Requests
- if current_user
- %span.count= MergeRequest.opened.of_group(@group).count
+ %span.count= number_with_delimiter(MergeRequest.opened.of_group(@group).count)
= nav_link(controller: [:group_members]) do
= link_to group_group_members_path(@group), title: 'Members' do
= icon('users fw')
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 64b30783c05..3b9d31a6fc5 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -17,7 +17,7 @@
= icon('gear fw')
%span
Account
- = nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new', 'applications#create']) do
+ = nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path, title: 'Applications' do
= icon('cloud fw')
%span
@@ -27,7 +27,7 @@
= icon('envelope-o fw')
%span
Emails
- %span.count= current_user.emails.count + 1
+ %span.count= number_with_delimiter(current_user.emails.count + 1)
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path, title: 'Password' do
@@ -45,7 +45,7 @@
= icon('key fw')
%span
SSH Keys
- %span.count= current_user.keys.count
+ %span.count= number_with_delimiter(current_user.keys.count)
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
-# TODO (rspeicher): Better icon?
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index c0d62028639..86b46e8c75e 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -16,7 +16,7 @@
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
- = icon('home fw')
+ = icon('bookmark fw')
%span
Project
= nav_link(path: 'projects#activity') do
@@ -25,7 +25,7 @@
%span
Activity
- if project_nav_tab? :files
- = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do
= icon('files-o fw')
%span
@@ -44,7 +44,7 @@
= icon('cubes fw')
%span
Builds
- %span.count.builds_counter= @project.builds.running_or_pending.count(:all)
+ %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all))
- if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do
@@ -67,7 +67,7 @@
%span
Issues
- if @project.default_issues_tracker?
- %span.count.issue_counter= @project.issues.opened.count
+ %span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
@@ -75,7 +75,7 @@
= icon('tasks fw')
%span
Merge Requests
- %span.count.merge_counter= @project.merge_requests.opened.count
+ %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
- if project_nav_tab? :settings
= nav_link(controller: [:project_members, :teams]) do
@@ -98,6 +98,13 @@
%span
Wiki
+ - if project_nav_tab? :forks
+ = nav_link(controller: :forks, action: :index) do
+ = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks' do
+ = icon('code-fork fw')
+ %span
+ Forks
+
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do
@@ -117,4 +124,3 @@
%li.hidden
= link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do
Network
-
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 970da78a5c9..dc3050f02e5 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -13,16 +13,22 @@
= icon('pencil-square-o fw')
%span
Project Settings
+ - if @project.allowed_to_share_with_group?
+ = nav_link(controller: :group_links) do
+ = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
+ = icon('share-square-o fw')
+ %span
+ Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
= icon('key fw')
%span
Deploy Keys
= nav_link(controller: :hooks) do
- = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do
+ = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
= icon('link fw')
%span
- Web Hooks
+ Webhooks
= nav_link(controller: :services) do
= link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
= icon('cogs fw')
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 3ca4c340406..37b4d562966 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -42,8 +42,15 @@
- else
#{link_to "View it on GitLab", @target_url}.
%br
- -# Don't link the host is the line below, one link in the email is easier to quickly click than two.
+ -# Don't link the host in the line below, one link in the email is easier to quickly click than two.
You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
- If you'd like to receive fewer emails, you can adjust your notification settings.
+ If you'd like to receive fewer emails, you can
+ - if @labels_url
+ adjust your #{link_to 'label subscriptions', @labels_url}.
+ - else
+ - if @sent_notification && @sent_notification.unsubscribable?
+ = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
+ from this thread or
+ adjust your notification settings.
- = email_action @target_url \ No newline at end of file
+ = email_action @target_url
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index abf73bcc709..ab527e8e438 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -1,6 +1,7 @@
-- page_title @project.name_with_namespace
-- header_title project_title(@project) unless header_title
-- sidebar "project" unless sidebar
+- page_title @project.name_with_namespace
+- page_description @project.description unless page_description
+- header_title project_title(@project) unless header_title
+- sidebar "project" unless sidebar
- content_for :scripts_body_top do
- project = @target_project || @project
diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml
index 00cb4aa24cc..12ded41fbf2 100644
--- a/app/views/notify/_note_message.html.haml
+++ b/app/views/notify/_note_message.html.haml
@@ -1,2 +1,5 @@
+- if current_application_settings.email_author_in_body
+ %div
+ #{link_to @note.author_name, user_url(@note.author)} wrote:
%div
= markdown(@note.note, pipeline: :email)
diff --git a/app/views/notify/_reassigned_issuable_email.text.erb b/app/views/notify/_reassigned_issuable_email.text.erb
index 855d37429d9..daf20a226dd 100644
--- a/app/views/notify/_reassigned_issuable_email.text.erb
+++ b/app/views/notify/_reassigned_issuable_email.text.erb
@@ -1,6 +1,6 @@
Reassigned <%= issuable.class.model_name.human.titleize %> <%= issuable.iid %>
-<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, {only_path: false}]) %>
+<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %>
diff --git a/app/views/notify/_relabeled_issuable_email.html.haml b/app/views/notify/_relabeled_issuable_email.html.haml
new file mode 100644
index 00000000000..80a0de255be
--- /dev/null
+++ b/app/views/notify/_relabeled_issuable_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ #{'Label'.pluralize(@label_names.size)} added:
+ %em= @label_names.to_sentence
diff --git a/app/views/notify/_relabeled_issuable_email.text.erb b/app/views/notify/_relabeled_issuable_email.text.erb
new file mode 100644
index 00000000000..6a83d79fd61
--- /dev/null
+++ b/app/views/notify/_relabeled_issuable_email.text.erb
@@ -0,0 +1,3 @@
+<%= 'Label'.pluralize(@label_names.size) %> added: <%= @label_names.to_sentence %>
+
+<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
index f4e9749e5c7..81d65037312 100644
--- a/app/views/notify/build_fail_email.html.haml
+++ b/app/views/notify/build_fail_email.html.haml
@@ -1,9 +1,10 @@
- content_for :header do
%h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
GitLab (build failed)
+
%h3
Project:
- = link_to ci_project_url(@project) do
+ = link_to namespace_project_url(@project.namespace, @project) do
= @project.name
%p
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
index 8b004d34cca..5d247eb4cf2 100644
--- a/app/views/notify/build_success_email.html.haml
+++ b/app/views/notify/build_success_email.html.haml
@@ -4,7 +4,7 @@
%h3
Project:
- = link_to ci_project_url(@project) do
+ = link_to namespace_project_url(@project.namespace, @project) do
= @project.name
%p
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index d3b799fca23..ad3ab2525bb 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -1,3 +1,6 @@
+- if current_application_settings.email_author_in_body
+ %div
+ #{link_to @issue.author_name, user_url(@issue.author)} wrote:
-if @issue.description
= markdown(@issue.description, pipeline: :email)
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 90ebdfc3fe2..23423e7d981 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -1,3 +1,6 @@
+- if current_application_settings.email_author_in_body
+ %div
+ #{link_to @merge_request.author_name, user_url(@merge_request.author)} wrote:
%p.details
!= merge_path_description(@merge_request, '&rarr;')
diff --git a/app/views/notify/relabeled_issue_email.html.haml b/app/views/notify/relabeled_issue_email.html.haml
new file mode 100644
index 00000000000..b17b16e1814
--- /dev/null
+++ b/app/views/notify/relabeled_issue_email.html.haml
@@ -0,0 +1 @@
+= render 'relabeled_issuable_email', issuable: @issue
diff --git a/app/views/notify/relabeled_issue_email.text.erb b/app/views/notify/relabeled_issue_email.text.erb
new file mode 100644
index 00000000000..eeced97f601
--- /dev/null
+++ b/app/views/notify/relabeled_issue_email.text.erb
@@ -0,0 +1 @@
+<%= render 'relabeled_issuable_email', issuable: @issue %>
diff --git a/app/views/notify/relabeled_merge_request_email.html.haml b/app/views/notify/relabeled_merge_request_email.html.haml
new file mode 100644
index 00000000000..9eaa9afa5b1
--- /dev/null
+++ b/app/views/notify/relabeled_merge_request_email.html.haml
@@ -0,0 +1 @@
+= render 'relabeled_issuable_email', issuable: @merge_request
diff --git a/app/views/notify/relabeled_merge_request_email.text.erb b/app/views/notify/relabeled_merge_request_email.text.erb
new file mode 100644
index 00000000000..87bc80ead32
--- /dev/null
+++ b/app/views/notify/relabeled_merge_request_email.text.erb
@@ -0,0 +1 @@
+<%= render 'relabeled_issuable_email', issuable: @merge_request %>
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index 4361f67a74d..f2e405b14fd 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -17,8 +17,8 @@
%strong #{link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))}
%div
%span by #{commit.author_name}
- %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
- %pre.commit-message
+ %i at #{commit.committed_date.to_s(:iso8601)}
+ %pre.commit-message
= commit.safe_message
%h4 #{pluralize @message.diffs_count, "changed file"}:
diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml
index aa0e263b6df..53869e36b28 100644
--- a/app/views/notify/repository_push_email.text.haml
+++ b/app/views/notify/repository_push_email.text.haml
@@ -8,7 +8,7 @@
\
= @message.reverse_compare? ? "Deleted commits:" : "Commits:"
- @message.commits.each do |commit|
- #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
+ #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.to_s(:iso8601)}
#{commit.safe_message}
\- - - - -
\
diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml
index 58af79716a7..879fc170f92 100644
--- a/app/views/profiles/_event_table.html.haml
+++ b/app/views/profiles/_event_table.html.haml
@@ -1,17 +1,15 @@
-.table-holder
- %table.table#audits
- %thead
- %tr
- %th Action
- %th When
+%h5.prepend-top-0
+ History of authentications
+
+%ul.well-list
+ - events.each do |event|
+ %li
+ %span.description
+ = audit_icon(event.details[:with], class: "append-right-5")
+ Signed in with
+ = event.details[:with]
+ authentication
+ %span.pull-right
+ #{time_ago_in_words event.created_at} ago
- %tbody
- - events.each do |event|
- %tr
- %td
- %span
- Signed in with
- %b= event.details[:with]
- authentication
- %td #{time_ago_in_words event.created_at} ago
= paginate events, theme: "gitlab"
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 17e47c622ce..6efd119f260 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -1,120 +1,117 @@
- page_title "Account"
- header_title page_title, profile_account_path
-- @blank_container = true
- if current_user.ldap_user?
.alert.alert-info
Some options are unavailable for LDAP accounts
-.account-page
- .panel.panel-default.update-token
- .panel-heading
- Reset Private token
- .panel-body
- = form_for @user, url: reset_private_token_profile_path, method: :put do |f|
- .data
- %p
- Your private token is used to access application resources without authentication.
- %br
- It can be used for atom feeds or the API.
- %span.cred
- Keep it secret!
-
- %p.cgray
- - if current_user.private_token
- = text_field_tag "token", current_user.private_token, class: "form-control"
- - else
- %span You don`t have one yet. Click generate to fix it.
-
- .form-actions
- - if current_user.private_token
- = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default"
- - else
- = f.submit 'Generate', class: "btn btn-default"
-
- - unless current_user.ldap_user?
- .panel.panel-default
- .panel-heading
- Two-factor Authentication
- .panel-body
- - if current_user.two_factor_enabled?
- .pull-right
- = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm',
- data: { confirm: 'Are you sure?' }
- %p.text-success
- %strong
- Two-factor Authentication is enabled
- %p
- If you lose your recovery codes you can
- %strong
- = succeed ',' do
- = link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' }
- invalidating all previous codes.
-
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Private Token
+ %p
+ Your private token is used to access application resources without authentication.
+ .col-lg-9
+ = form_for @user, url: reset_private_token_profile_path, method: :put, html: {class: "private-token"} do |f|
+ %p.cgray
+ - if current_user.private_token
+ = label_tag "token", "Private token", class: "label-light"
+ = text_field_tag "token", current_user.private_token, class: "form-control"
- else
- %p
- Increase your account's security by enabling two-factor authentication (2FA).
- %p
- Each time you log in you’ll be required to provide your username and
- password as usual, plus a randomly-generated code from your phone.
-
- .form-actions
- = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success'
-
- - if button_based_providers.any?
- .panel.panel-default
- .panel-heading
+ %span You don`t have one yet. Click generate to fix it.
+ %p.help-block
+ It can be used for atom feeds or the API. Keep it secret!
+ .prepend-top-default
+ - if current_user.private_token
+ = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default"
+ - else
+ = f.submit 'Generate', class: "btn btn-default"
+%hr
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Two-factor Authentication
+ %p
+ Increase your account's security by enabling two-factor authentication (2FA).
+ .col-lg-9
+ %p
+ Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'}
+ - if !current_user.two_factor_enabled?
+ %p
+ Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
+ More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
+ .append-bottom-10
+ = link_to 'Enable two-factor authentication', new_profile_two_factor_auth_path, class: 'btn btn-success'
+ - else
+ = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger',
+ data: { confirm: 'Are you sure?' }
+%hr
+- if button_based_providers.any?
+ .row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Social sign-in
+ %p
+ Activate signin with one of the following services
+ .col-lg-9
+ %label.label-light
Connected Accounts
- .panel-body
- .oauth-buttons.append-bottom-10
- %p Click on icon to activate signin with one of the following services
- - button_based_providers.each do |provider|
- .btn-group
- = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: "btn btn-lg #{'active' if auth_active?(provider)}", "data-no-turbolink" => "true"
-
- - if auth_active?(provider)
- = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do
- = icon('close')
-
- - if current_user.can_change_username?
- .panel.panel-warning.update-username
- .panel-heading
- Change Username
- .panel-body
- = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f|
- %p
- Changing your username will change path to all personal projects!
- %div
- .input-group
- .input-group-addon
- = "#{root_url}u/"
- = f.text_field :username, required: true, class: 'form-control'
- &nbsp;
- .loading-gif.hide
- %p
- = icon('spinner spin')
- Saving new username
- .form-actions
- = f.submit 'Save username', class: "btn btn-warning"
+ %p Click on icon to activate signin with one of the following services
+ - button_based_providers.each do |provider|
+ .provider-btn-group
+ .provider-btn-image
+ = provider_image_tag(provider)
+ - if auth_active?(provider)
+ = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
+ Disconnect
+ - else
+ = link_to user_omniauth_authorize_path(provider), method: :post, class: "provider-btn #{'not-active' if !auth_active?(provider)}", "data-no-turbolink" => "true" do
+ Connect
+ %hr
+- if current_user.can_change_username?
+ .row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0.change-username-title
+ Change username
+ %p
+ Changing your username will change path to all personal projects!
+ .col-lg-9
+ = form_for @user, url: update_username_profile_path, method: :put, remote: true, html: {class: "update-username"} do |f|
+ .form-group
+ = f.label :username, "Path", class: "label-light"
+ .input-group
+ .input-group-addon
+ = "#{root_url}u/"
+ = f.text_field :username, required: true, class: 'form-control'
+ .help-block
+ Current path:
+ = "#{root_url}u/#{current_user.username}"
+ .prepend-top-default
+ = f.button class: "btn btn-warning", type: "submit" do
+ = icon "spinner spin", class: "hidden loading-username"
+ Update username
+ %hr
- - if signup_enabled?
- .panel.panel-danger.remove-account
- .panel-heading
+- if signup_enabled?
+ .row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0.remove-account-title
Remove account
- .panel-body
- - if @user.can_be_removed?
- %p Deleting an account has the following effects:
- %ul
- %li All user content like authored issues, snippets, comments will be removed
- - rp = current_user.personal_projects.count
- - unless rp.zero?
- %li #{pluralize rp, 'personal project'} will be removed and cannot be restored
- .form-actions
- = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
- - else
- - if @user.solo_owned_groups.present?
- %p
- Your account is currently an owner in these groups:
- %strong #{@user.solo_owned_groups.map(&:name).join(', ')}
- %p
- You must transfer ownership or delete these groups before you can delete your account.
+ .col-lg-9
+ - if @user.can_be_removed?
+ %p
+ Deleting an account has the following effects:
+ %ul
+ %li All user content like authored issues, snippets, comments will be removed
+ - rp = current_user.personal_projects.count
+ - unless rp.zero?
+ %li #{pluralize rp, 'personal project'} will be removed and cannot be restored
+ = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
+ - else
+ - if @user.solo_owned_groups.present?
+ %p
+ Your account is currently an owner in these groups:
+ %strong #{@user.solo_owned_groups.map(&:name).join(', ')}
+ %p
+ You must transfer ownership or delete these groups before you can delete your account.
+.append-bottom-default
diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml
deleted file mode 100644
index 0436c2213da..00000000000
--- a/app/views/profiles/applications.html.haml
+++ /dev/null
@@ -1,70 +0,0 @@
-- page_title "Applications"
-- header_title page_title, applications_profile_path
-
-.gray-content-block.top-block
- - if user_oauth_applications?
- Manage applications that can use GitLab as an OAuth provider,
- and applications that you've authorized to use your account.
- - else
- Manage applications that you've authorized to use your account.
-
-- if user_oauth_applications?
- .oauth-applications
- %h3
- Your applications
- .pull-right
- = link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
- - if @applications.any?
- .table-holder
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Callback URL
- %th Clients
- %th
- %th
- %tbody
- - @applications.each do |application|
- %tr{:id => "application_#{application.id}"}
- %td= link_to application.name, oauth_application_path(application)
- %td
- - application.redirect_uri.split.each do |uri|
- %div= uri
- %td= application.access_tokens.count
- %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm'
- %td= render 'doorkeeper/applications/delete_form', application: application
-
-.oauth-authorized-applications.prepend-top-20
- - if user_oauth_applications?
- %h3
- Authorized applications
-
- - if @authorized_tokens.any?
- .table-holder
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Authorized At
- %th Scope
- %th
- %tbody
- - @authorized_apps.each do |app|
- - token = app.authorized_tokens.order('created_at desc').first
- %tr{:id => "application_#{app.id}"}
- %td= app.name
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', application: app
- - @authorized_anonymous_tokens.each do |token|
- %tr
- %td
- Anonymous
- %div.help-block
- %em Authorization was granted by entering your username and password in the application.
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', token: token
- - else
- %p.light You don't have any authorized applications
diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml
index 8fdba45b193..f630c03e5f6 100644
--- a/app/views/profiles/audit_log.html.haml
+++ b/app/views/profiles/audit_log.html.haml
@@ -1,8 +1,11 @@
- page_title "Audit Log"
- header_title page_title, audit_log_profile_path
-.gray-content-block.top-block
- History of authentications
-
-.prepend-top-default
-= render 'event_table', events: @events
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h3.prepend-top-0
+ = page_title
+ %p
+ This is a security log of important events involving your account.
+ .col-lg-9
+ = render 'event_table', events: @events
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 1d140347a5f..3f328f96cea 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,52 +1,49 @@
- page_title "Emails"
- header_title page_title, profile_emails_path
-.gray-content-block.top-block
- Control emails linked to your account
-
-%ul.prepend-top-default
- %li
- Your
- %b Primary Email
- will be used for avatar detection and web based operations, such as edits and merges.
- %li
- Your
- %b Notification Email
- will be used for account notifications.
- %li
- Your
- %b Public Email
- will be displayed on your public profile.
- %li
- All email addresses will be used to identify your commits.
-
-.panel.panel-default
- .panel-heading
- Emails (#{@emails.count + 1})
- %ul.well-list#emails-table
- %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|
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ Control emails linked to your account
+ .col-lg-9
+ %h4.prepend-top-0
+ Add email address
+ = form_for 'email', url: profile_emails_path do |f|
+ .form-group
+ = f.label :email, class: 'label-light'
+ = f.text_field :email, class: 'form-control'
+ .prepend-top-default
+ = f.submit 'Add email address', class: 'btn btn-create'
+ %hr
+ %h4.prepend-top-0
+ Linked emails (#{@emails.count + 1})
+ .account-well.append-bottom-default
+ %ul
+ %li
+ Your Primary Email will be used for avatar detection and web based operations, such as edits and merges.
+ %li
+ Your Notification Email will be used for account notifications.
+ %li
+ Your Public Email will be displayed on your public profile.
+ %li
+ All email addresses will be used to identify your commits.
+ %ul.well-list
%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-sm btn-remove pull-right'
-
-%h4 Add email address
-= form_for 'email', url: profile_emails_path, html: { class: 'form-horizontal' } do |f|
- .form-group
- = f.label :email, class: 'control-label'
- .col-sm-10
- = f.text_field :email, class: 'form-control'
- .form-actions
- = f.submit 'Add email address', class: 'btn btn-create'
+ = @primary
+ %span.pull-right
+ %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
+ = email.email
+ %span.pull-right
+ - 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
+ = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right'
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 2a8800de60e..4d78215ed3c 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -1,5 +1,5 @@
%div
- = form_for [:profile, @key], html: { class: 'form-horizontal js-requires-input' } do |f|
+ = form_for [:profile, @key], html: { class: 'js-requires-input' } do |f|
- if @key.errors.any?
.alert.alert-danger
%ul
@@ -7,13 +7,11 @@
%li= msg
.form-group
- = f.label :key, class: 'control-label'
- .col-sm-10
- = f.text_area :key, class: "form-control", rows: 8, autofocus: true, required: true
+ = f.label :key, class: 'label-light'
+ = f.text_area :key, class: "form-control", rows: 8, required: true
.form-group
- = f.label :title, class: 'control-label'
- .col-sm-10= f.text_field :title, class: "form-control", required: true
+ = f.label :title, class: 'label-light'
+ = f.text_field :title, class: "form-control", required: true
- .form-actions
+ .prepend-top-default
= f.submit 'Add key', class: "btn btn-create"
- = link_to "Cancel", profile_keys_path, class: "btn btn-cancel"
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 9bbccbc45ea..25e9e8ff008 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -1,11 +1,14 @@
-%tr
- %td
- = link_to path_to_key(key, is_admin) do
- %strong= key.title
- %td
- %code.key-fingerprint= key.fingerprint
- %td
- %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-sm btn-remove delete-key pull-right"
+%li.key-list-item
+ .pull-left.append-right-10
+ = icon 'key', class: "key-icon hidden-xs"
+ .key-list-item-info
+ = link_to path_to_key(key, is_admin), class: "title" do
+ = key.title
+ .description
+ = key.fingerprint
+ .pull-right
+ %span.key-created-at
+ created #{time_ago_with_tooltip(key.created_at)} ago
+ = link_to path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent prepend-left-10" do
+ %span.sr-only Remove
+ = icon('trash')
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 0ca8bd95157..dd7615400dc 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -1,5 +1,5 @@
- is_admin = defined?(admin) ? true : false
-.row
+.row.prepend-top-default
.col-md-4
.panel.panel-default
.panel-heading
@@ -10,7 +10,7 @@
%strong= @key.title
%li
%span.light Created on:
- %strong= @key.created_at.stamp("Aug 21, 2011")
+ %strong= @key.created_at.to_s(:medium)
.col-md-8
%p
diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml
index 8c9d546af4c..296cafa6e31 100644
--- a/app/views/profiles/keys/_key_table.html.haml
+++ b/app/views/profiles/keys/_key_table.html.haml
@@ -1,19 +1,11 @@
-- is_admin = defined?(admin) ? true : false
+- is_admin = local_assigns.fetch(:admin, false)
+
- if @keys.any?
- .table-holder
- %table.table
- %thead.panel-heading
- %tr
- %th Title
- %th Fingerprint
- %th Added at
- %th
- %tbody
- - @keys.each do |key|
- = render 'profiles/keys/key', key: key, is_admin: is_admin
+ %ul.well-list
+ = render partial: 'profiles/keys/key', collection: @keys, locals: { is_admin: is_admin }
- else
- .nothing-here-block
+ %p.profile-settings-message.text-center
- if is_admin
- User has no ssh keys
+ There are no SSH keys associated with this account.
- else
There are no SSH keys with access to your account.
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 17a4195030e..e0f8c9a5733 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,14 +1,21 @@
- page_title "SSH Keys"
- header_title page_title, profile_keys_path
-.gray-content-block.top-block
- .pull-right
- = link_to new_profile_key_path, class: "btn btn-new" do
- = icon('plus')
- Add SSH Key
- .oneline
- Before you can add an SSH key you need to
- = link_to "generate it.", help_page_path("ssh", "README")
-
-.prepend-top-default
-= render 'key_table'
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ SSH keys allow you to establish a secure connection between your computer and GitLab.
+ .col-lg-9
+ %h5.prepend-top-0
+ Add an SSH key
+ %p.profile-settings-content
+ Before you can add an SSH key you need to
+ = link_to "generate it.", help_page_path("ssh", "README")
+ = render 'form'
+ %hr
+ %h5
+ Your SSH keys (#{@keys.count})
+ %div.append-bottom-default
+ = render 'key_table'
diff --git a/app/views/profiles/keys/new.html.haml b/app/views/profiles/keys/new.html.haml
deleted file mode 100644
index 13a18269d11..00000000000
--- a/app/views/profiles/keys/new.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-- 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")}.
-%hr
-= render 'form'
-
-:javascript
- $('#key_key').on('focusout', function(){
- var title = $('#key_title'),
- val = $('#key_key').val(),
- comment = val.match(/^\S+ \S+ (.+)\n?$/);
-
- if( comment && comment.length > 1 && title.val() == '' ){
- $('#key_title').val( comment[1] ).change();
- }
- });
diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml
index 742c5c4b68d..d0d044136f6 100644
--- a/app/views/profiles/notifications/_settings.html.haml
+++ b/app/views/profiles/notifications/_settings.html.haml
@@ -1,5 +1,5 @@
-%li
- %span.notification.fa.fa-holder
+%li.notification-list-item
+ %span.notification.fa.fa-holder.append-right-5
- if notification.global?
= notification_icon(@notification)
- else
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 0bcadc965fa..de80abd7f4d 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -1,11 +1,7 @@
- page_title "Notifications"
- header_title page_title, profile_notifications_path
-.gray-content-block.top-block
- These are your global notification settings.
-
-.prepend-top-default
-= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications form-horizontal global-notifications-form' } do |f|
+= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
-if @user.errors.any?
%div.alert.alert-danger
%ul
@@ -13,65 +9,66 @@
%li= msg
= hidden_field_tag :notification_type, 'global'
+ .row
+ .col-lg-3.profile-settings-sidebar
+ %h4
+ = page_title
+ %p
+ You can specify notification level per group or per project.
+ %p
+ By default, all projects and groups will use the global notifications setting.
+ .col-lg-9
+ %h5
+ Global notification settings
+ .form-group
+ = f.label :notification_email, class: "label-light"
+ = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
+ .form-group
+ = f.label :notification_level, class: 'label-light'
+ .radio
+ = f.label :notification_level, value: Notification::N_DISABLED do
+ = f.radio_button :notification_level, Notification::N_DISABLED
+ .level-title
+ Disabled
+ %p You will not get any notifications via email
- .form-group
- = f.label :notification_email, class: "control-label"
- .col-sm-10
- = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "form-control"
-
- .form-group
- = f.label :notification_level, class: 'control-label'
- .col-sm-10
- .radio
- = f.label :notification_level, value: Notification::N_DISABLED do
- = f.radio_button :notification_level, Notification::N_DISABLED
- .level-title
- Disabled
- %p You will not get any notifications via email
-
- .radio
- = f.label :notification_level, value: Notification::N_MENTION do
- = f.radio_button :notification_level, Notification::N_MENTION
- .level-title
- On Mention
- %p You will receive notifications only for comments in which you were @mentioned
-
- .radio
- = f.label :notification_level, value: Notification::N_PARTICIPATING do
- = f.radio_button :notification_level, Notification::N_PARTICIPATING
- .level-title
- Participating
- %p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
-
- .radio
- = f.label :notification_level, value: Notification::N_WATCH do
- = f.radio_button :notification_level, Notification::N_WATCH
- .level-title
- Watch
- %p You will receive notifications for any activity
+ .radio
+ = f.label :notification_level, value: Notification::N_MENTION do
+ = f.radio_button :notification_level, Notification::N_MENTION
+ .level-title
+ On Mention
+ %p You will receive notifications only for comments in which you were @mentioned
- .gray-content-block
- = f.submit 'Save changes', class: "btn btn-create"
+ .radio
+ = f.label :notification_level, value: Notification::N_PARTICIPATING do
+ = f.radio_button :notification_level, Notification::N_PARTICIPATING
+ .level-title
+ Participating
+ %p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
-.row.all-notifications.prepend-top-default
- .col-md-6
- %p
- You can also specify notification level per group or per project.
- %br
- By default, all projects and groups will use the notification level set above.
- %h4 Groups:
- %ul.bordered-list
- - @group_members.each do |group_member|
- - notification = Notification.new(group_member)
- = render 'settings', type: 'group', membership: group_member, notification: notification
+ .radio
+ = f.label :notification_level, value: Notification::N_WATCH do
+ = f.radio_button :notification_level, Notification::N_WATCH
+ .level-title
+ Watch
+ %p You will receive notifications for any activity
- .col-md-6
- %p
- To specify the notification level per project of a group you belong to,
- %br
- you need to be a member of the project itself, not only its group.
- %h4 Projects:
- %ul.bordered-list
- - @project_members.each do |project_member|
- - notification = Notification.new(project_member)
- = render 'settings', type: 'project', membership: project_member, notification: notification
+ .prepend-top-default
+ = f.submit 'Update settings', class: "btn btn-create"
+ %hr
+ %h5
+ Groups (#{@group_members.count})
+ %div
+ %ul.bordered-list
+ - @group_members.each do |group_member|
+ - notification = Notification.new(group_member)
+ = render 'settings', type: 'group', membership: group_member, notification: notification
+ %h5
+ Projects (#{@project_members.count})
+ %p.account-well
+ To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group.
+ .append-bottom-default
+ %ul.bordered-list
+ - @project_members.each do |project_member|
+ - notification = Notification.new(project_member)
+ = render 'settings', type: 'project', membership: project_member, notification: notification
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index fab7c45c9b2..afd4f996b62 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -1,20 +1,18 @@
- page_title "Password"
- header_title page_title, edit_profile_password_path
-.gray-content-block.top-block
- - if @user.password_automatically_set?
- Set your password.
- - else
- Change your password or recover your current one.
-
-.update-password.prepend-top-default
- = form_for @user, url: profile_password_path, method: :put, html: { class: 'form-horizontal' } do |f|
- %div
- %p.slead
- - unless @user.password_automatically_set?
- You must provide current password in order to change it.
- %br
- After a successful password update, you will be redirected to the login page where you can log in with your new password.
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ After a successful password update, you will be redirected to the login page where you can log in with your new password.
+ .col-lg-9
+ %h5.prepend-top-0
+ Change your password
+ - unless @user.password_automatically_set?
+ or recover your current one
+ = form_for @user, url: profile_password_path, method: :put, html: {class: "update-password"} do |f|
-if @user.errors.any?
.alert.alert-danger
%ul
@@ -22,19 +20,16 @@
%li= msg
- unless @user.password_automatically_set?
.form-group
- = f.label :current_password, class: 'control-label'
- .col-sm-10
- = f.password_field :current_password, required: true, class: 'form-control'
- %div
- = link_to "Forgot your password?", reset_profile_password_path, method: :put
-
- .form-group
- = f.label :password, 'New password', class: 'control-label'
- .col-sm-10
+ = f.label :current_password, class: 'label-light'
+ = f.password_field :current_password, required: true, class: 'form-control'
+ %p.help-block
+ You must provide your current password in order to change it.
+ .form-group
+ = f.label :password, 'New password', class: 'label-light'
= f.password_field :password, required: true, class: 'form-control'
- .form-group
- = f.label :password_confirmation, class: 'control-label'
- .col-sm-10
+ .form-group
+ = f.label :password_confirmation, class: 'label-light'
= f.password_field :password_confirmation, required: true, class: 'form-control'
- .form-actions
- = f.submit 'Save password', class: "btn btn-create"
+ .prepend-top-default.append-bottom-default
+ = f.submit 'Save password', class: "btn btn-create append-right-10"
+ = link_to "I forgot my password", reset_profile_password_path, method: :put, class: "account-btn-link"
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 877589dc390..f80211669fb 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,57 +1,56 @@
- page_title 'Preferences'
- header_title page_title, profile_preferences_path
-- @blank_container = true
-.alert.alert-help
- These settings allow you to customize the appearance and behavior of the site.
- They are saved with your account and will persist to any device you use to
- access the site.
-
-= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'js-preferences-form form-horizontal'} do |f|
- .panel.panel-default.application-theme
- .panel-heading
+= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'row prepend-top-default js-preferences-form'} do |f|
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
Application theme
- .panel-body
- - Gitlab::Themes.each do |theme|
- = label_tag do
- .preview{class: theme.css_class}
- = f.radio_button :theme_id, theme.id
- = theme.name
-
- .panel.panel-default.syntax-theme
- .panel-heading
+ %p
+ This setting allows you to customize the appearance of the site, ex. sidebar.
+ .col-lg-9.application-theme
+ - Gitlab::Themes.each do |theme|
+ = label_tag do
+ .preview{class: theme.css_class}
+ = f.radio_button :theme_id, theme.id
+ = theme.name
+ .col-sm-12
+ %hr
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
Syntax highlighting theme
- .panel-body
- - Gitlab::ColorSchemes.each do |scheme|
- = label_tag do
- .preview= image_tag "#{scheme.css_class}-scheme-preview.png"
- = f.radio_button :color_scheme_id, scheme.id
- = scheme.name
-
- .panel.panel-default
- .panel-heading
+ %p
+ This setting allow you to customize the appearance of the syntax.
+ .col-lg-9.syntax-theme
+ - Gitlab::ColorSchemes.each do |scheme|
+ = label_tag do
+ .preview= image_tag "#{scheme.css_class}-scheme-preview.png"
+ = f.radio_button :color_scheme_id, scheme.id
+ = scheme.name
+ .col-sm-12
+ %hr
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
Behavior
- .panel-body
- .form-group
- = f.label :layout, class: 'control-label' do
- Layout width
- .col-sm-10
- = f.select :layout, layout_choices, {}, class: 'form-control'
- .help-block
- Choose between fixed (max. 1200px) and fluid (100%) application layout.
- .form-group
- = f.label :dashboard, class: 'control-label' do
- Default Dashboard
- = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank')
- .col-sm-10
- = f.select :dashboard, dashboard_choices, {}, class: 'form-control'
- .form-group
- = f.label :project_view, class: 'control-label' do
- Project view
- = link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank')
- .col-sm-10
- = f.select :project_view, project_view_choices, {}, class: 'form-control'
- .help-block
- Choose what content you want to see on a project's home page.
- .panel-footer
+ %p
+ This setting allows you to customize the behavior of the system layout and default views.
+ .col-lg-9
+ .form-group
+ = f.label :layout, class: 'label-light' do
+ Layout width
+ = f.select :layout, layout_choices, {}, class: 'form-control'
+ .help-block
+ Choose between fixed (max. 1200px) and fluid (100%) application layout.
+ .form-group
+ = f.label :dashboard, class: 'label-light' do
+ Default Dashboard
+ = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank')
+ = f.select :dashboard, dashboard_choices, {}, class: 'form-control'
+ .form-group
+ = f.label :project_view, class: 'label-light' do
+ Project view
+ = link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank')
+ = f.select :project_view, project_view_choices, {}, class: 'form-control'
+ .help-block
+ Choose what content you want to see on a project's home page.
+ .form-group
= f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 9459d8a6295..cd582ba7060 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,101 +1,96 @@
-.gray-content-block.top-block
- This information will appear on your profile.
- - if current_user.ldap_user?
- Some options are unavailable for LDAP accounts
-
-.prepend-top-default
-= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit_user form-horizontal" }, authenticity_token: true do |f|
+= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f|
-if @user.errors.any?
%div.alert.alert-danger
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
.row
- .col-md-7
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Public Avatar
+ %p
+ - if @user.avatar?
+ You can change your avatar here
+ - if Gitlab.config.gravatar.enabled
+ or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
+ - else
+ You can upload an avatar here
+ - if Gitlab.config.gravatar.enabled
+ or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
+ .col-lg-9
+ .clearfix.avatar-image.append-bottom-default
+ = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
+ %h5.prepend-top-0
+ Upload new avatar
+ .prepend-top-5.append-bottom-10
+ %a.btn.js-choose-user-avatar-button
+ Browse file...
+ %span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen
+ = f.file_field :avatar, class: "js-user-avatar-input hidden"
+ .help-block
+ 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-gray"
+ %hr
+ .row
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Main settings
+ %p
+ This information will appear on your profile.
+ - if current_user.ldap_user?
+ Some options are unavailable for LDAP accounts
+ .col-lg-9
.form-group
- = f.label :name, class: "control-label"
- .col-sm-10
- = f.text_field :name, class: "form-control", required: true
- %span.help-block Enter your name, so people you know can recognize you.
+ = f.label :name, class: "label-light"
+ = f.text_field :name, class: "form-control", required: true
+ %span.help-block Enter your name, so people you know can recognize you.
.form-group
- = f.label :email, class: "control-label"
- .col-sm-10
- - if @user.ldap_user?
- = f.text_field :email, class: "form-control", required: true, readonly: true
- %span.help-block.light
- Email is read-only for LDAP user
+ = f.label :email, class: "label-light"
+ - if @user.ldap_user? && @user.ldap_email?
+ = f.text_field :email, class: "form-control", required: true, readonly: true
+ %span.help-block.light
+ Your email address was automatically set based on the LDAP server.
+ - else
+ - if @user.temp_oauth_email?
+ = f.text_field :email, class: "form-control", required: true, value: nil
- else
- - if @user.temp_oauth_email?
- = f.text_field :email, class: "form-control", required: true, value: nil
- - else
- = f.text_field :email, class: "form-control", required: true
- - if @user.unconfirmed_email.present?
- %span.help-block
- Please click the link in the confirmation email before continuing. It was sent to
- = succeed "." do
- %strong #{@user.unconfirmed_email}
- %p
- = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
+ = f.text_field :email, class: "form-control", required: true
+ - if @user.unconfirmed_email.present?
+ %span.help-block
+ Please click the link in the confirmation email before continuing. It was sent to
+ = succeed "." do
+ %strong #{@user.unconfirmed_email}
+ %p
+ = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
- - else
- %span.help-block We also use email for avatar detection if no avatar is uploaded.
+ - 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 on profile'}, class: "select2"
- %span.help-block This email will be displayed on your public profile.
+ = f.label :public_email, class: "label-light"
+ = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2"
+ %span.help-block This email will be displayed on your public profile.
.form-group
- = f.label :skype, class: "control-label"
- .col-sm-10= f.text_field :skype, class: "form-control"
+ = f.label :skype, class: "label-light"
+ = f.text_field :skype, class: "form-control"
.form-group
- = f.label :linkedin, class: "control-label"
- .col-sm-10= f.text_field :linkedin, class: "form-control"
+ = f.label :linkedin, class: "label-light"
+ = f.text_field :linkedin, class: "form-control"
.form-group
- = f.label :twitter, class: "control-label"
- .col-sm-10= f.text_field :twitter, class: "form-control"
+ = f.label :twitter, class: "label-light"
+ = f.text_field :twitter, class: "form-control"
.form-group
- = f.label :website_url, 'Website', class: "control-label"
- .col-sm-10= f.text_field :website_url, class: "form-control"
+ = f.label :website_url, 'Website', class: "label-light"
+ = 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"
+ = f.label :location, 'Location', class: "label-light"
+ = 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
- %span.help-block Tell us about yourself in fewer than 250 characters.
-
- .col-md-5
- .light-well
- = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
-
- .clearfix
- .profile-avatar-form-option
- %p.light
- - if @user.avatar?
- You can change your avatar here
- - if Gitlab.config.gravatar.enabled
- %br
- or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
- - else
- You can upload an avatar here
- - if Gitlab.config.gravatar.enabled
- %br
- or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
- %hr
- %a.choose-btn.btn.btn-sm.js-choose-user-avatar-button
- %i.fa.fa-paperclip
- %span Choose File ...
- &nbsp;
- %span.file_name.js-avatar-filename File name...
- = f.file_field :avatar, class: "js-user-avatar-input hidden"
- .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-sm remove-avatar"
-
-
- .form-actions
- = f.submit 'Save changes', class: "btn btn-success"
- = link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
+ = f.label :bio, class: "label-light"
+ = f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
+ %span.help-block Tell us about yourself in fewer than 250 characters.
+ .prepend-top-default.append-bottom-default
+ = f.submit 'Update profile settings', class: "btn btn-success"
+ = link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml
index 1a5b6efce35..5d342ef58e5 100644
--- a/app/views/profiles/two_factor_auths/new.html.haml
+++ b/app/views/profiles/two_factor_auths/new.html.haml
@@ -1,41 +1,41 @@
- page_title 'Two-factor Authentication', 'Account'
-%h2.page-title Two-Factor Authentication (2FA)
-%p
- Download the Google Authenticator application from App Store for iOS or Google
- Play for Android and scan this code.
-
- More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
-
-%hr
-
-= form_tag profile_two_factor_auth_path, method: :post, class: 'form-horizontal two-factor-new' do |f|
- - if @error
- .alert.alert-danger
- = @error
- .form-group
- .col-lg-2.col-lg-offset-2
- = raw @qr_code
- .col-lg-7.col-lg-offset-1.manual-instructions
- %h3 Can't scan the code?
-
- %p
- To add the entry manually, provide the following details to the
- application on your phone.
-
- %dl
- %dt Account
- %dd= current_user.email
- %dl
- %dt Key
- %dd= current_user.otp_secret.scan(/.{4}/).join(' ')
- %dl
- %dt Time based
- %dd Yes
- .form-group
- = label_tag :pin_code, nil, class: "control-label"
- .col-lg-10
- = text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true
- .form-actions
- = submit_tag 'Submit', class: 'btn btn-success'
- = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
+.row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Two-factor Authentication (2FA)
+ %p
+ Increase your account's security by enabling two-factor authentication (2FA).
+ .col-lg-9
+ %p
+ Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'}
+ %p
+ Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
+ More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
+ .row.append-bottom-10
+ .col-md-3
+ = raw @qr_code
+ .col-md-9
+ .account-well
+ %p.prepend-top-0.append-bottom-0
+ Can't scan the code?
+ %p.prepend-top-0.append-bottom-0
+ To add the entry manually, provide the following details to the application on your phone.
+ %p.prepend-top-0.append-bottom-0
+ Account:
+ = current_user.email
+ %p.prepend-top-0.append-bottom-0
+ Key:
+ = current_user.otp_secret.scan(/.{4}/).join(' ')
+ %p.two-factor-new-manual-content
+ Time based: Yes
+ = form_tag profile_two_factor_auth_path, method: :post do |f|
+ - if @error
+ .alert.alert-danger
+ = @error
+ .form-group
+ = label_tag :pin_code, nil, class: "label-light"
+ = text_field_tag :pin_code, nil, class: "form-control", required: true
+ .prepend-top-default
+ = submit_tag 'Enable two-factor authentication', class: 'btn btn-success'
+ = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 101880bd105..961b61d2e76 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,6 +1,6 @@
-.gray-content-block.activity-filter-block
+.nav-block.activity-filter-block
- if current_user
- .pull-right
+ .controls
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
%i.fa.fa-rss
diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml
new file mode 100644
index 00000000000..95ab9ecf3e8
--- /dev/null
+++ b/app/views/projects/_builds_settings.html.haml
@@ -0,0 +1,60 @@
+%fieldset.builds-feature
+ %legend
+ Builds:
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ %p Get recent application code using the following command:
+ .radio
+ = f.label :build_allow_git_fetch_false do
+ = f.radio_button :build_allow_git_fetch, 'false'
+ %strong git clone
+ %br
+ %span.descr Slower but makes sure you have a clean dir before every build
+ .radio
+ = f.label :build_allow_git_fetch_true do
+ = f.radio_button :build_allow_git_fetch, 'true'
+ %strong git fetch
+ %br
+ %span.descr Faster
+
+ .form-group
+ = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label'
+ .col-sm-10
+ = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
+ %p.help-block per build in minutes
+ .form-group
+ = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label'
+ .col-sm-10
+ .input-group
+ %span.input-group-addon /
+ = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
+ %span.input-group-addon /
+ %p.help-block
+ We will use this regular expression to find test coverage output in build trace.
+ Leave blank if you want to disable this feature
+ .bs-callout.bs-callout-info
+ %p Below are examples of regex for existing tools:
+ %ul
+ %li
+ Simplecov (Ruby) -
+ %code \(\d+.\d+\%\) covered
+ %li
+ pytest-cov (Python) -
+ %code \d+\%\s*$
+ %li
+ phpunit --coverage-text --colors=never (PHP) -
+ %code ^\s*Lines:\s*\d+.\d+\%
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :public_builds do
+ = f.check_box :public_builds
+ %strong Public builds
+ .help-block Allow everyone to access builds for Public and Internal projects
+
+ .form-group
+ = f.label :runners_token, "Runners token", class: 'control-label'
+ .col-sm-10
+ = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
+ %p.help-block The secure token used to checkout project.
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index fa978325ddd..96c2fa87f45 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -1,5 +1,5 @@
#tree-holder.tree-holder.clearfix
- .gray-content-block.second-block
+ .nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml
new file mode 100644
index 00000000000..08e2fc48be7
--- /dev/null
+++ b/app/views/projects/_find_file_link.html.haml
@@ -0,0 +1,3 @@
+= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do
+ = icon('search')
+ %span Find File
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index e92115b9b98..b45df44f270 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -3,7 +3,12 @@
.project-identicon-holder
= project_icon(@project, alt: '', class: 'project-avatar avatar s90')
.project-home-desc
- %h1= @project.name
+ %h1
+ = @project.name
+ %span.visibility-icon.has_tooltip{data: { container: 'body' },
+ title: "#{visibility_level_label(@project.visibility_level)} - #{project_visibility_level_description(@project.visibility_level)}"}
+ = visibility_level_icon(@project.visibility_level, fw: false)
+
- if @project.description.present?
= markdown(@project.description, pipeline: :description)
@@ -12,32 +17,44 @@
Forked from
= link_to project_path(forked_from_project) do
= forked_from_project.namespace.try(:name)
- .cover-controls.left
- .visibility-level-label.has_tooltip{title: project_visibility_level_description(@project.visibility_level), data: { container: 'body' } }
- = visibility_level_icon(@project.visibility_level, fw: false)
- = visibility_level_label(@project.visibility_level)
.cover-controls
- - if can?(current_user, :admin_project, @project)
- = link_to edit_project_path(@project), class: 'btn btn-gray' do
- = icon('pencil')
- if current_user
- &nbsp;
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), class: 'btn btn-gray' do
= icon('rss')
+ - access = user_max_access_in_project(current_user.id, @project)
+ - can_edit = can?(current_user, :admin_project, @project)
+ - if access || can_edit
+ %span.dropdown.project-settings-dropdown
+ %a.dropdown-new.btn.btn-gray#project-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ = icon('cog')
+ = icon('angle-down')
+ %ul.dropdown-menu.dropdown-menu-right
+ - if can_edit
+ %li
+ = link_to edit_project_path(@project) do
+ Edit Project
+ - if access
+ %li
+ = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
+ data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do
+ Leave Project
.project-repo-buttons
.split-one.count-buttons
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- = render "shared/clone_panel"
+ .clone-row
+ .project-clone-holder
+ = render "shared/clone_panel"
- .split-repo-buttons
- = render "projects/buttons/download"
- = render 'projects/buttons/dropdown'
+ .split-repo-buttons
+ .btn-group.pull-left
+ = render "projects/buttons/download"
+ = render 'projects/buttons/dropdown'
- = render 'projects/buttons/notifications'
+ = render 'projects/buttons/notifications'
-:coffeescript
- new Star() \ No newline at end of file
+:javascript
+ new Star();
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 54c818baaf4..1fb37ef6621 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,6 +1,6 @@
.md-area
.md-header.clearfix
- %ul.center-top-menu
+ %ul.nav-links
%li.active
%a.js-md-write-button(href="#md-write-holder" tabindex="-1")
Write
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 7e6301abde8..e701253d7de 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,13 +1,12 @@
.zennable
- %input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox")
.zen-backdrop
- - classes << ' js-gfm-input markdown-area'
+ - classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f
- = f.text_area attr, class: classes, placeholder: ''
+ = f.text_area attr, class: classes
- else
- = text_area_tag attr, nil, class: classes, placeholder: ''
- %a.zen-enter-link(tabindex="-1" href="#")
+ = text_area_tag attr, nil, class: classes
+ %a.js-zen-enter(tabindex="-1" href="#")
= icon('expand')
Edit in fullscreen
- %a.zen-leave-link(href="#")
+ %a.js-zen-leave(tabindex="-1" href="#")
= icon('compress')
diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml
new file mode 100644
index 00000000000..def493c56f5
--- /dev/null
+++ b/app/views/projects/artifacts/_tree_directory.html.haml
@@ -0,0 +1,8 @@
+- path_to_directory = browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path)
+
+%tr.tree-item{ 'data-link' => path_to_directory}
+ %td.tree-item-file-name
+ = tree_icon('folder', '755', directory.name)
+ %span.str-truncated
+ = link_to directory.name, path_to_directory
+ %td
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
new file mode 100644
index 00000000000..36fb4c998c9
--- /dev/null
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -0,0 +1,9 @@
+- path_to_file = file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: file.path)
+
+%tr.tree-item{ 'data-link' => path_to_file }
+ %td.tree-item-file-name
+ = tree_icon('file', '664', file.name)
+ %span.str-truncated
+ = link_to file.name, path_to_file
+ %td
+ = number_to_human_size(file.metadata[:size], precision: 2)
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
new file mode 100644
index 00000000000..84034c8bf16
--- /dev/null
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -0,0 +1,22 @@
+- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
+= render 'projects/builds/header_title'
+
+.top-block.gray-content-block.clearfix
+ .pull-right
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
+ class: 'btn btn-default download' do
+ = icon('download')
+ Download artifacts archive
+
+.tree-holder
+ %div.tree-content-holder
+ %table.table.tree-table
+ %thead
+ %tr
+ %th Name
+ %th Size
+ = render partial: 'tree_directory', collection: @entry.directories(parent: true), as: :directory
+ = render partial: 'tree_file', collection: @entry.files, as: :file
+
+- if @entry.empty?
+ .center Empty
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 8d9ec068a43..5f9a92ff93f 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -3,7 +3,7 @@
%h3.page-title Blame view
-#tree-holder.tree-holder
+#blob-content-holder.tree-holder
.file-holder
.file-title
= blob_icon @blob.mode, @blob.name
@@ -12,14 +12,14 @@
%small= number_to_human_size @blob.size
.file-actions
= render "projects/blob/actions"
- .file-content.blame.highlight
+ .file-content.blame.code.js-syntax-highlight
%table
- current_line = 1
- - @blame.each do |blame_group|
+ - @blame_groups.each do |blame_group|
%tr
%td.blame-commit
.commit
- - commit = Commit.new(blame_group[:commit], @project)
+ - commit = blame_group[:commit]
.commit-row-title
%strong
= link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark"
@@ -30,16 +30,16 @@
= commit_author_link(commit, avatar: false)
authored
#{time_ago_with_tooltip(commit.committed_date, skip_js: true)}
- %td.lines.blame-numbers
- %pre
- - line_count = blame_group[:lines].count
- - (current_line...(current_line + line_count)).each do |i|
+ %td.line-numbers
+ - line_count = blame_group[:lines].count
+ - (current_line...(current_line + line_count)).each do |i|
+ %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
+ = icon("link")
= i
- \
- - current_line += line_count
+ \
+ - current_line += line_count
%td.lines
- %pre{class: 'code highlight white'}
+ %pre.code.highlight
%code
- blame_group[:lines].each do |line|
- :erb
- <%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %>
+ #{line}
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 2a3315da3db..3ffc3fcb7ac 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -1,4 +1,4 @@
-.gray-content-block.top-block
+.nav-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'blob', path: @path
@@ -32,11 +32,4 @@
= number_to_human_size(blob_size(blob))
.file-actions.hidden-xs
= render "actions"
- - if blob.lfs_pointer?
- = render "download", blob: blob
- - elsif blob.text?
- = render "text", blob: blob
- - elsif blob.image?
- = render "image", blob: blob
- - else
- = render "download", blob: blob
+ = render blob, blob: blob
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 10b02813733..f8b6fa253c4 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -10,7 +10,7 @@
%span.editor-file-name
\/
= text_field_tag 'file_name', params[:file_name], placeholder: "File name",
- required: true, class: 'form-control new-file-name js-quick-submit'
+ required: true, class: 'form-control new-file-name'
.pull-right
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index c090f690d1d..18caddabd39 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -1,2 +1,9 @@
.file-content.image_file
- %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
+ - if blob.svg?
+ - # We need to scrub SVG but we cannot do so in the RawController: it would
+ - # be wrong/strange if RawController modified the data.
+ - blob.load_all_data!(@repository)
+ - blob = sanitize_svg(blob)
+ %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
+ - else
+ %img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))}
diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml
index 084608bbba3..84694203d7d 100644
--- a/app/views/projects/blob/_new_dir.html.haml
+++ b/app/views/projects/blob/_new_dir.html.haml
@@ -5,7 +5,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title Create New Directory
.modal-body
- = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do
+ = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-quick-submit js-requires-input' do
.form-group
= label_tag :dir_name, 'Directory name', class: 'control-label'
.col-sm-10
diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml
index 1cf19a7d3db..2e1f32fd15e 100644
--- a/app/views/projects/blob/_remove.html.haml
+++ b/app/views/projects/blob/_remove.html.haml
@@ -6,7 +6,7 @@
%h3.page-title Delete #{@blob.name}
.modal-body
- = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-requires-input' do
+ = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-quick-submit js-requires-input' do
= render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}"
.form-group
diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml
index 4429c395aee..d09cd73558c 100644
--- a/app/views/projects/blob/_text.html.haml
+++ b/app/views/projects/blob/_text.html.haml
@@ -1,9 +1,10 @@
+- blob.load_all_data!(@repository)
- if markup?(blob.name)
.file-content.wiki
= render_markup(blob.name, blob.data)
- else
- .file-content.code
- - unless blob.empty?
- = render 'shared/file_highlight', blob: blob
- - else
+ - unless blob.empty?
+ = render 'shared/file_highlight', blob: blob
+ - else
+ .file-content.code
.nothing-here-block Empty file
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 676924dc6ca..b1f50eb5f34 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -5,7 +5,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title #{title}
.modal-body
- = form_tag form_path, method: method, class: 'js-upload-blob-form form-horizontal' do
+ = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do
.dropzone
.dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index f3b01ff3288..abcfca4cd11 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -10,8 +10,9 @@
%tr.line_holder
%td.old_line.diff-line-num{data: {linenumber: line_old}}
= link_to raw(line_old), "#"
- %td.new_line= link_to raw(line_new) , "#"
- %td.line_content.noteable_line= ' ' * @form.indent + line
+ %td.new_line.diff-line-num
+ = link_to raw(line_new) , "#"
+ %td.line_content.noteable_line==#{' ' * @form.indent}#{line}
- if @form.unfold? && @form.bottom? && @form.to < @blob.loc
%tr.line_holder{ id: @form.to }
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 09fa148b129..effcce5a1c4 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -2,7 +2,7 @@
= render "header_title"
.file-editor
- %ul.center-top-menu.no-bottom.js-edit-mode
+ %ul.nav-links.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
= icon('edit')
@@ -13,7 +13,7 @@
= icon('eye')
= editing_preview_title(@blob.name)
- = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input js-edit-blob-form') do
+ = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 167fa615182..1dd2b5c0af7 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -5,7 +5,7 @@
New File
.file-editor
- = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-requires-input') do
+ = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-quick-submit js-requires-input') do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"
diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml
index e7c3460ad78..541dc96c45f 100644
--- a/app/views/projects/blob/preview.html.haml
+++ b/app/views/projects/blob/preview.html.haml
@@ -8,18 +8,18 @@
.file-content.wiki
= raw render_markup(@blob.name, @content)
- else
- .file-content.code
+ .file-content.code.js-syntax-highlight
- unless @diff_lines.empty?
%table.text-file
- @diff_lines.each do |line|
%tr.line_holder{ class: "#{line.type}" }
- if line.type == "match"
- %td.old_line= "..."
- %td.new_line= "..."
- %td.line_content.matched= line.text
+ %td.old_line.diff-line-num= "..."
+ %td.new_line.diff-line-num= "..."
+ %td.line_content.match= line.text
- else
- %td.old_line
- %td.new_line
- %td.line_content{class: "#{line.type}"}= raw diff_line_content(line.text)
+ %td.old_line.diff-line-num
+ %td.new_line.diff-line-num
+ %td.line_content{class: "#{line.type}"}= diff_line_content(line.text)
- else
.nothing-here-block No changes.
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 5081bae6801..76a823d3828 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -1,8 +1,12 @@
- commit = @repository.commit(branch.target)
+- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
+- diverging_commit_counts = @repository.diverging_commit_counts(branch)
+- number_commits_behind = diverging_commit_counts[:behind]
+- number_commits_ahead = diverging_commit_counts[:ahead]
%li(class="js-branch-#{branch.name}")
%div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do
- %strong.str-truncated= branch.name
+ %span.item-title.str-truncated= branch.name
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
@@ -29,6 +33,17 @@
= link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
+ - if branch.name != @repository.root_ref
+ .divergence-graph{ title: "#{number_commits_ahead} commits ahead, #{number_commits_behind} commits behind #{@repository.root_ref}" }
+ .graph-side
+ .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
+ %span.count.count-behind= number_commits_behind
+ .graph-separator
+ .graph-side
+ .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
+ %span.count.count-ahead= number_commits_ahead
+
+
- if commit
= render 'projects/branches/commit', commit: commit, project: @project
- else
diff --git a/app/views/projects/branches/destroy.js.haml b/app/views/projects/branches/destroy.js.haml
index 882a4d0c5e2..a21ddaf4930 100644
--- a/app/views/projects/branches/destroy.js.haml
+++ b/app/views/projects/branches/destroy.js.haml
@@ -1 +1 @@
-$('.js-totalbranch-count').html("#{@repository.branches.size}")
+$('.js-totalbranch-count').html("#{@repository.branch_count}")
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 204def60794..7afea5a5049 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -10,7 +10,7 @@
&nbsp;
.dropdown.inline
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light sort:
+ %span.light
- if @sort.present?
= @sort.humanize
- else
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 1a26908ab11..811d304ea75 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -1,16 +1,16 @@
- page_title "Builds"
= render "header_title"
-.project-issuable-filter
- .controls
- - if can?(current_user, :manage_builds, @project)
- .pull-left.hidden-xs
- - if @all_builds.running_or_pending.any?
- = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
-
- %ul.center-top-menu
+.top-area
+ %ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to project_builds_path(@project) do
+ All
+ %span.badge.js-totalbuilds-count
+ = number_with_delimiter(@all_builds.count(:id))
+
+ %li{class: ('active' if @scope == 'running')}
+ = link_to project_builds_path(@project, scope: :running) do
Running
%span.badge.js-running-count
= number_with_delimiter(@all_builds.running_or_pending.count(:id))
@@ -21,11 +21,15 @@
%span.badge.js-running-count
= number_with_delimiter(@all_builds.finished.count(:id))
- %li{class: ('active' if @scope == 'all')}
- = link_to project_builds_path(@project, scope: :all) do
- All
- %span.badge.js-totalbuilds-count
- = number_with_delimiter(@all_builds.count(:id))
+ .nav-controls
+ - if can?(current_user, :update_build, @project)
+ - if @all_builds.running_or_pending.any?
+ = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project),
+ data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
+
+ = link_to ci_lint_path, class: 'btn btn-default' do
+ = icon('wrench')
+ %span CI Lint
.gray-content-block
#{(@scope || 'running').capitalize} builds from this project
@@ -40,16 +44,17 @@
%thead
%tr
%th Status
- %th Runner
+ %th Build ID
%th Commit
%th Ref
%th Stage
%th Name
%th Duration
%th Finished at
+ - if @project.build_coverage_enabled?
+ %th Coverage
%th
- - @builds.each do |build|
- = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, allow_retry: true
+ = render @builds, commit_sha: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled?
= paginate @builds, theme: 'gitlab'
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 5b7ecce86ab..b02aee3db21 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -13,9 +13,10 @@
= link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request)
#up-build-trace
- - if @commit.matrix_for_ref?(@build.ref)
- %ul.center-top-menu.no-top.no-bottom
- - @commit.latest_builds_for_ref(@build.ref).each do |build|
+ - builds = @build.commit.matrix_builds(@build)
+ - if builds.size > 1
+ %ul.nav-links.no-top.no-bottom
+ - builds.each do |build|
%li{class: ('active' if build == @build) }
= link_to namespace_project_build_path(@project.namespace, @project, build) do
= ci_icon_for_status(build.status)
@@ -44,7 +45,7 @@
.pull-right
#{time_ago_with_tooltip(@build.finished_at) if @build.finished_at}
- - if @build.show_warning?
+ - if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning
%p
@@ -70,16 +71,22 @@
.autoscroll-container
%button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
.clearfix
- .scroll-controls
+ #js-build-scroll.scroll-controls
= link_to '#up-build-trace', class: 'btn' do
%i.fa.fa-angle-up
= link_to '#down-build-trace', class: 'btn' do
%i.fa.fa-angle-down
- %pre.trace#build-trace
- %code.bash
- = preserve do
- = raw @build.trace_html
+ - if @build.erased?
+ .erased.alert.alert-warning
+ - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
+ Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
+ - else
+ %pre.trace#build-trace
+ %code.bash
+ = preserve do
+ = raw @build.trace_html
+
%div#down-build-trace
.col-md-3
@@ -89,37 +96,60 @@
Test coverage
%h1 #{@build.coverage}%
- - if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url
- .build-widget.center
- = link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary'
+ - if can?(current_user, :read_build, @project) && @build.artifacts?
+ .build-widget.artifacts
+ %h4.title Build artifacts
+ .center
+ .btn-group{ role: :group }
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
+ = icon('download')
+ Download
+
+ - if @build.artifacts_metadata?
+ = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
+ = icon('folder-open')
+ Browse
.build-widget
%h4.title
Build ##{@build.id}
- - if current_user && can?(current_user, :manage_builds, @project)
- .pull-right
- - if @build.cancel_url
- = link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post
- - elsif @build.retry_url
- = link_to "Retry", @build.retry_url, class: 'btn btn-sm btn-primary', method: :post
-
- - if @build.duration
+ - if can?(current_user, :update_build, @project)
+ .center
+ .btn-group{ role: :group }
+ - if @build.active?
+ = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger', method: :post
+ - elsif @build.retryable?
+ = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post
+
+ - if @build.erasable?
+ = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
+ class: 'btn btn-sm btn-warning', method: :post,
+ data: { confirm: 'Are you sure you want to erase this build?' } do
+ = icon('eraser')
+ Erase
+
+ .clearfix
+ - if @build.duration
+ %p
+ %span.attr-name Duration:
+ #{duration_in_words(@build.finished_at, @build.started_at)}
%p
- %span.attr-name Duration:
- #{duration_in_words(@build.finished_at, @build.started_at)}
- %p
- %span.attr-name Created:
- #{time_ago_with_tooltip(@build.created_at)}
- - if @build.finished_at
+ %span.attr-name Created:
+ #{time_ago_with_tooltip(@build.created_at)}
+ - if @build.finished_at
+ %p
+ %span.attr-name Finished:
+ #{time_ago_with_tooltip(@build.finished_at)}
+ - if @build.erased_at
+ %p
+ %span.attr-name Erased:
+ #{time_ago_with_tooltip(@build.erased_at)}
%p
- %span.attr-name Finished:
- #{time_ago_with_tooltip(@build.finished_at)}
- %p
- %span.attr-name Runner:
- - if @build.runner && current_user && current_user.admin
- = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
- - elsif @build.runner
- \##{@build.runner.id}
+ %span.attr-name Runner:
+ - if @build.runner && current_user && current_user.admin
+ = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
+ - elsif @build.runner
+ \##{@build.runner.id}
- if @build.trigger_request
.build-widget
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 14ee2263b7d..6a60cfeff76 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,4 +1,4 @@
- unless @project.empty_repo?
- if can? current_user, :download_code, @project
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', rel: 'nofollow', title: "Download ZIP" do
+ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do
= icon('download')
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 1f639fecc30..e7c85edff96 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,6 +1,6 @@
- if current_user
- %span.dropdown
- %a.dropdown-new.btn.btn-new{href: '#', "data-toggle" => "dropdown"}
+ .btn-group
+ %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- if can?(current_user, :create_issue, @project)
@@ -8,9 +8,10 @@
= link_to url_for_new_issue(@project, only_path: true) do
= icon('exclamation-circle fw')
New issue
- - if can?(current_user, :create_merge_request, @project)
+ - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - if merge_project
%li
- = link_to new_namespace_project_merge_request_path(@project.namespace, @project) do
+ = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do
= icon('tasks fw')
New merge request
- if can?(current_user, :create_snippet, @project)
@@ -45,7 +46,7 @@
- continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
- - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
+ - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
= icon('file fw')
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
new file mode 100644
index 00000000000..d22d1da8402
--- /dev/null
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -0,0 +1,76 @@
+%tr.build
+ %td.status
+ - if can?(current_user, :read_build, build)
+ = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
+ - else
+ = ci_status_with_icon(build.status)
+
+ %td.build-link
+ - if can?(current_user, :read_build, build)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
+ %strong ##{build.id}
+ - else
+ %strong ##{build.id}
+
+ - if build.stuck?
+ %i.fa.fa-warning.text-warning
+
+ - if defined?(commit_sha) && commit_sha
+ %td
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
+
+ %td
+ - if build.ref
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
+ - else
+ .light none
+
+ - if defined?(runner) && runner
+ %td
+ - if build.try(:runner)
+ = runner_link(build.runner)
+ - else
+ .light none
+
+ - if defined?(stage) && stage
+ %td
+ = build.stage
+
+ %td
+ = build.name
+
+ .pull-right
+ - if build.tags.any?
+ - build.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if build.try(:trigger_request)
+ %span.label.label-info triggered
+ - if build.try(:allow_failure)
+ %span.label.label-danger allowed to fail
+
+ %td.duration
+ - if build.duration
+ #{duration_in_words(build.finished_at, build.started_at)}
+
+ %td.timestamp
+ - if build.finished_at
+ %span #{time_ago_with_tooltip(build.finished_at)}
+
+ - if defined?(coverage) && coverage
+ %td.coverage
+ - if build.try(:coverage)
+ #{build.coverage}%
+
+ %td
+ .pull-right
+ - if can?(current_user, :read_build, build) && build.artifacts?
+ = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
+ %i.fa.fa-download
+ - if can?(current_user, :update_build, build)
+ - if build.active?
+ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
+ %i.fa.fa-remove.cred
+ - elsif defined?(allow_retry) && allow_retry && build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
+ %i.fa.fa-repeat
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
index 329aaa0bb8b..003b7c18d0e 100644
--- a/app/views/projects/commit/_builds.html.haml
+++ b/app/views/projects/commit/_builds.html.haml
@@ -1,6 +1,6 @@
.gray-content-block.middle-block
.pull-right
- - if can?(current_user, :manage_builds, @ci_commit.project)
+ - if can?(current_user, :update_build, @ci_commit.project)
- if @ci_commit.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post
@@ -43,8 +43,8 @@
%th Coverage
%th
- @ci_commit.refs.each do |ref|
- = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered,
- locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true }
+ - builds = @ci_commit.statuses.for_ref(ref).latest.ordered
+ = render builds, coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true
- if @ci_commit.retried.any?
.gray-content-block.second-block
@@ -64,5 +64,4 @@
- if @ci_commit.project.build_coverage_enabled?
%th Coverage
%th
- = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried,
- locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true }
+ = render @ci_commit.retried, coverage: @ci_commit.project.build_coverage_enabled?, stage: true
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index f74f8b427ec..ea33aa472a6 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu.no-top.no-bottom.commit-ci-menu
+%ul.nav-links.no-top.no-bottom.commit-ci-menu
= nav_link(path: 'commit#show') do
= link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Changes
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index ddb77fd796b..71995fcc487 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -16,6 +16,8 @@
= link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped" do
= icon('files-o')
Browse Files
+ - unless @commit.has_been_reverted?(current_user)
+ = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id))
%div
%p
@@ -50,7 +52,7 @@
.commit-info-row.branches
%i.fa.fa-spinner.fa-spin
-.commit-box.gray-content-block.middle-block
+.commit-box.content-block
%h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line
- if @commit.description.present?
diff --git a/app/views/projects/commit/_revert.html.haml b/app/views/projects/commit/_revert.html.haml
new file mode 100644
index 00000000000..52ca3ed5b14
--- /dev/null
+++ b/app/views/projects/commit/_revert.html.haml
@@ -0,0 +1,31 @@
+#modal-revert-commit.modal
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %h3.page-title== Revert this #{revert_commit_type(commit)}
+ .modal-body
+ = form_tag revert_namespace_project_commit_path(@project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do
+ .form-group.branch
+ = label_tag 'target_branch', 'Revert in branch', class: 'control-label'
+ .col-sm-10
+ = select_tag "target_branch", grouped_options_refs, class: "select2 select2-sm js-target-branch"
+ - if can?(current_user, :push_code, @project)
+ .js-create-merge-request-container
+ .checkbox
+ - nonce = SecureRandom.hex
+ = label_tag "create_merge_request-#{nonce}" do
+ = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
+ Start a <strong>new merge request</strong> with these changes
+ - else
+ = hidden_field_tag 'create_merge_request', 1
+ .form-actions
+ = submit_tag "Revert", class: 'btn btn-create'
+ = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+
+ - unless can?(current_user, :push_code, @project)
+ .inline.prepend-left-10
+ = commit_in_fork_help
+
+:javascript
+ new NewCommitForm($('.js-create-dir-form'))
diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml
index 99d62503a94..7118a4846c6 100644
--- a/app/views/projects/commit/builds.html.haml
+++ b/app/views/projects/commit/builds.html.haml
@@ -1,6 +1,7 @@
- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits"
= render "projects/commits/header_title"
-= render "commit_box"
+.prepend-top-default
+ = render "commit_box"
= render "ci_menu"
= render "builds"
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 069b8b1f169..21e186120c3 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -1,9 +1,16 @@
-- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
+- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
+- page_description @commit.description
+
= render "projects/commits/header_title"
-= render "commit_box"
+
+.prepend-top-default
+ = render "commit_box"
- if @ci_commit
= render "ci_menu"
- else
%div.block-connector
-= render "projects/diffs/diffs", diffs: @diffs, project: @project
+= render "projects/diffs/diffs", diffs: @diffs, project: @project,
+ diff_refs: @diff_refs
= render "projects/notes/notes_with_form"
+- if can_collaborate_with_project?
+ = render "projects/commit/revert", commit: @commit, title: @commit.title
diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml
deleted file mode 100644
index 74a05df24d3..00000000000
--- a/app/views/projects/commit_statuses/_commit_status.html.haml
+++ /dev/null
@@ -1,79 +0,0 @@
-%tr.commit_status
- %td.status
- - if commit_status.target_url
- = link_to commit_status.target_url, class: "ci-status ci-#{commit_status.status}" do
- = ci_icon_for_status(commit_status.status)
- = commit_status.status
- - else
- = ci_status_with_icon(commit_status.status)
-
- %td.commit_status-link
- - if commit_status.target_url
- = link_to commit_status.target_url do
- %strong ##{commit_status.id}
- - else
- %strong ##{commit_status.id}
-
- - if commit_status.show_warning?
- %i.fa.fa-warning.text-warning
-
- - if defined?(commit_sha) && commit_sha
- %td
- = link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace"
-
- %td
- - if commit_status.ref
- = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref)
- - else
- .light none
-
- - if defined?(runner) && runner
- %td
- - if commit_status.try(:runner)
- = runner_link(commit_status.runner)
- - else
- .light none
-
- - if defined?(stage) && stage
- %td
- = commit_status.stage
-
- %td
- = commit_status.name
-
- .pull-right
- - if commit_status.tags.any?
- - commit_status.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if commit_status.try(:trigger_request)
- %span.label.label-info triggered
- - if commit_status.try(:allow_failure)
- %span.label.label-danger allowed to fail
-
- %td.duration
- - if commit_status.duration
- #{duration_in_words(commit_status.finished_at, commit_status.started_at)}
-
- %td.timestamp
- - if commit_status.finished_at
- %span #{time_ago_with_tooltip(commit_status.finished_at)}
-
- - if defined?(coverage) && coverage
- %td.coverage
- - if commit_status.try(:coverage)
- #{commit_status.coverage}%
-
- %td
- .pull-right
- - if current_user && can?(current_user, :download_build_artifacts, commit_status.project) && commit_status.download_url
- = link_to commit_status.download_url, title: 'Download artifacts' do
- %i.fa.fa-download
- - if current_user && can?(current_user, :manage_builds, commit_status.project)
- - if commit_status.active?
- - if commit_status.cancel_url
- = link_to commit_status.cancel_url, method: :post, title: 'Cancel' do
- %i.fa.fa-remove.cred
- - elsif defined?(allow_retry) && allow_retry && commit_status.retry_url
- = link_to commit_status.retry_url, method: :post, title: 'Retry' do
- %i.fa.fa-repeat
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 012825f0fdb..7f2903589a9 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -11,7 +11,7 @@
= cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
.commit-row-title
- %strong.str-truncated
+ %span.item-title.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
- if commit.description?
%a.text-expander.js-toggle-button ...
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index ce60fbdf032..bac9e244d36 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -1,11 +1,14 @@
+- commits, hidden = limited_commits(@commits)
+- commits = Commit.decorate(commits, @project)
+
%div.panel.panel-default
.panel-heading
Commits (#{@commits.count})
- - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
+ - if hidden > 0
%ul.well-list
- - Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), @project).each do |commit|
+ - commits.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.
+ #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else
- %ul.well-list= render Commit.decorate(@commits, @project), project: @project
+ %ul.well-list= render commits, project: @project
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 0cd9ce1f371..a7e3c2478c2 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -1,15 +1,21 @@
- unless defined?(project)
- project = @project
-- @commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits|
+- commits, hidden = limited_commits(@commits)
+
+- commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits|
.row.commits-row
.col-md-2.hidden-xs.hidden-sm
%h5.commits-row-date
%i.fa.fa-calendar
- %span= day.stamp("28 Aug, 2010")
+ %span= day.strftime('%d %b, %Y')
.light
= pluralize(commits.count, 'commit')
.col-md-10.col-sm-12
%ul.bordered-list
= render commits, project: project
%hr.lists-separator
+
+- if hidden > 0
+ .alert.alert-warning
+ #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index fcccb002d7e..7a5b0d993db 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(controller: [:commit, :commits]) do
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
Commits
@@ -15,9 +15,9 @@
= nav_link(html_options: {class: branches_tab_class}) do
= link_to namespace_project_branches_path(@project.namespace, @project) do
Branches
- %span.badge.js-totalbranch-count= @repository.branches.size
+ %span.badge.js-totalbranch-count= @repository.branch_count
= nav_link(controller: [:tags, :releases]) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
Tags
- %span.badge.js-totaltags-count= @repository.tags.length
+ %span.badge.js-totaltags-count= @repository.tag_count
diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder
index 7ffa7317196..e310fafd82c 100644
--- a/app/views/projects/commits/show.atom.builder
+++ b/app/views/projects/commits/show.atom.builder
@@ -4,14 +4,14 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html"
xml.id namespace_project_commits_url(@project.namespace, @project, @ref)
- xml.updated @commits.first.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") if @commits.any?
+ xml.updated @commits.first.committed_date.xmlschema if @commits.any?
@commits.each do |commit|
xml.entry do
xml.id namespace_project_commit_url(@project.namespace, @project, id: commit.id)
xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id)
xml.title truncate(commit.title, length: 80)
- xml.updated commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")
+ xml.updated commit.committed_date.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email))
xml.author do |author|
xml.name commit.author_name
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 2dd99cc8215..c52cf25d40a 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -6,30 +6,37 @@
= render "head"
-.gray-content-block
+.gray-content-block.second-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
- .commits-feed-holder.hidden-xs.hidden-sm
- - if create_mr_button?(@repository.root_ref, @ref)
- = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
- = icon('plus')
- Create Merge Request
+ .block-controls.hidden-xs.hidden-sm
+ - if @merge_request.present?
+ .control
+ = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn'
+ - elsif create_mr_button?(@repository.root_ref, @ref)
+ .control
+ = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
+ = icon('plus')
+ Create Merge Request
+
+ .control
+ = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do
+ = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false }
- if current_user && current_user.private_token
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'prepend-left-10 btn' do
- = icon("rss")
+ .control
+ = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do
+ = icon("rss")
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
%div{id: dom_id(@project)}
- #commits-list= render "commits", project: @project
+ #commits-list.content_list= render "commits", project: @project
.clear
= spinner
-- if @commits.count == @limit
- :javascript
- CommitsList.init("#{@ref}", #{@limit});
-
+:javascript
+ CommitsList.init("#{@ref}", #{@limit});
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index efc25eda26b..4ab81f3635c 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -13,12 +13,13 @@
= text_field_tag :to, params[:to], class: "form-control", required: true
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
- - if create_mr_button?
+ - if @merge_request.present?
+ = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'prepend-left-10 btn'
+ - elsif create_mr_button?
= link_to create_mr_path, class: 'prepend-left-10 btn' do
= icon("plus")
Create Merge Request
-
:javascript
var availableTags = #{@project.repository.ref_names.to_json};
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 51088a7dea8..da731f28bb6 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -9,7 +9,7 @@
- if @commits.present?
.prepend-top-default
= render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs, project: @project
+ = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs
- else
.light-well.prepend-top-default
.center
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index f9d661d59d2..6086ad3661e 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,17 +1,17 @@
- if diff_view == 'parallel'
- fluid_layout true
-- diff_files = safe_diff_files(diffs)
+- diff_files = safe_diff_files(diffs, diff_refs)
-.gray-content-block.middle-block.oneline-block
+.content-block.oneline-block
.inline-parallel-buttons
.btn-group
= inline_diff_btn
= parallel_diff_btn
= render 'projects/diffs/stats', diff_files: diff_files
-- if diff_files.count < diffs.size
- = render 'projects/diffs/warning', diffs: diffs, shown_files_count: diff_files.count
+- if diff_files.overflow?
+ = render 'projects/diffs/warning', diff_files: diff_files
.files
- diff_files.each_with_index do |diff_file, index|
@@ -21,10 +21,3 @@
= render 'projects/diffs/file', i: index, project: project,
diff_file: diff_file, diff_commit: diff_commit, blob: blob
-
-- if @diff_timeout
- .alert.alert-danger
- %h4
- Failed to collect changes
- %p
- Maybe diff is really big and operation failed with timeout. Try to get diff locally
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 517f6aef7c5..dc34032b1b8 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,52 +1,58 @@
-.diff-file{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)}
- .diff-header{id: "file-path-#{hexdigest(diff_file.file_path)}"}
+.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)}
+ .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"}
- if diff_file.diff.submodule?
%span
= icon('archive fw')
%strong
= submodule_link(blob, @commit.id, project.repository)
- else
- %span
- = blob_icon blob.mode, blob.name
- = link_to "#diff-#{i}" do
- %strong
- = diff_file.new_path
+ = blob_icon blob.mode, blob.name
- - if diff_file.deleted_file
- deleted
- - elsif diff_file.renamed_file
- renamed from
+ = link_to "#diff-#{i}" do
+ - if diff_file.renamed_file
+ - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
+ %strong.filename.old
+ = old_path
+ &rarr;
+ %strong.filename.new
+ = new_path
+ - else
%strong
- = diff_file.old_path
+ = diff_file.new_path
+ - if diff_file.deleted_file
+ deleted
- - if diff_file.mode_changed?
- %small
- = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
+ - if diff_file.mode_changed?
+ %small
+ = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
- .diff-controls
+ .file-actions.hidden-xs
- if blob_text_viewable?(blob)
- = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do
- %i.fa.fa-comments
- &nbsp;
+ = link_to '#', class: 'js-toggle-diff-comments btn active has_tooltip', title: "Toggle comments for this file" do
+ = icon('comments')
+ \
- if editable_diff?(diff_file)
= edit_blob_link(@merge_request.source_project,
@merge_request.source_branch, diff_file.new_path,
from_merge_request_id: @merge_request.id)
- &nbsp;
= view_file_btn(diff_commit.id, diff_file, project)
.diff-content.diff-wrap-lines
-# Skipp all non non-supported blobs
- return unless blob.respond_to?('text?')
- - if blob_text_viewable?(blob)
- - if diff_view == 'parallel'
- = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
- - else
- = render "projects/diffs/text_file", diff_file: diff_file, index: i
- - elsif blob.image?
- - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file)
- = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i
+ - if diff_file.too_large?
+ .nothing-here-block
+ This diff could not be displayed because it is too large.
- else
- .nothing-here-block No preview for this file type
+ - if blob_text_viewable?(blob)
+ - if diff_view == 'parallel'
+ = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
+ - else
+ = render "projects/diffs/text_file", diff_file: diff_file, index: i
+ - elsif blob.image?
+ - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file)
+ = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i
+ - else
+ .nothing-here-block No preview for this file type
diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml
index 058b71b21f5..752e92e2e6b 100644
--- a/app/views/projects/diffs/_image.html.haml
+++ b/app/views/projects/diffs/_image.html.haml
@@ -1,9 +1,11 @@
- diff = diff_file.diff
+- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))
+- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))
- if diff.renamed_file || diff.new_file || diff.deleted_file
.image
%span.wrap
.frame{class: image_diff_class(diff)}
- %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
+ %img{src: diff.deleted_file ? old_file_raw_path : file_raw_path}
%p.image-info= "#{number_to_human_size file.size}"
- else
.image
@@ -11,7 +13,7 @@
%span.wrap
.frame.deleted
%a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))}
- %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"}
+ %img{src: old_file_raw_path}
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size old_file.size}"
|
@@ -23,7 +25,7 @@
%span.wrap
.frame.added
%a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))}
- %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
+ %img{src: file_raw_path}
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size file.size}"
|
@@ -36,10 +38,10 @@
%div.swipe.view.hide
.swipe-frame
.frame.deleted
- %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"}
+ %img{src: old_file_raw_path}
.swipe-wrap
.frame.added
- %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
+ %img{src: file_raw_path}
%span.swipe-bar
%span.top-handle
%span.bottom-handle
@@ -47,9 +49,9 @@
%div.onion-skin.view.hide
.onion-skin-frame
.frame.deleted
- %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"}
+ %img{src: old_file_raw_path}
.frame.added
- %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
+ %img{src: file_raw_path}
.controls
.transparent
.drag-track
diff --git a/app/views/projects/diffs/_match_line.html.haml b/app/views/projects/diffs/_match_line.html.haml
index d1f897b99f7..d6dddd97879 100644
--- a/app/views/projects/diffs/_match_line.html.haml
+++ b/app/views/projects/diffs/_match_line.html.haml
@@ -4,4 +4,4 @@
%td.new_line.diff-line-num{data: {linenumber: line_new},
class: [unfold_bottom_class(bottom), unfold_class(!new_file)]}
\...
-%td.line_content.matched= line
+%td.line_content.match= line
diff --git a/app/views/projects/diffs/_match_line_parallel.html.haml b/app/views/projects/diffs/_match_line_parallel.html.haml
index 815df16aa4a..0cd888876e0 100644
--- a/app/views/projects/diffs/_match_line_parallel.html.haml
+++ b/app/views/projects/diffs/_match_line_parallel.html.haml
@@ -1,4 +1,4 @@
-%td.old_line
- %td.line_content.parallel.matched= line
-%td.new_line
- %td.line_content.parallel.matched= line
+%td.old_line.diff-line-num
+%td.line_content.parallel.match= line
+%td.new_line.diff-line-num
+%td.line_content.parallel.match= line
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 37fd1b1ec8a..d7c49068745 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,42 +1,40 @@
/ Side-by-side diff view
-%div.text-file.diff-wrap-lines
+%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight
%table
- - parallel_diff(diff_file, index).each do |line|
- - type_left = line[0]
- - line_number_left = line[1]
- - line_content_left = line[2]
- - line_code_left = line[3]
- - type_right = line[4]
- - line_number_right = line[5]
- - line_content_right = line[6]
- - line_code_right = line[7]
-
+ - diff_file.parallel_diff_lines.each do |line|
+ - left = line[:left]
+ - right = line[:right]
%tr.line_holder.parallel
- - if type_left == 'match'
- = render "projects/diffs/match_line_parallel", { line: line_content_left,
- line_old: line_number_left, line_new: line_number_right }
- - elsif type_left == 'old' || type_left.nil?
- %td.old_line{id: line_code_left, class: "#{type_left}"}
- = link_to raw(line_number_left), "##{line_code_left}", id: line_code_left
+ - if left[:type] == 'match'
+ = render "projects/diffs/match_line_parallel", { line: left[:text],
+ line_old: left[:number], line_new: right[:number] }
+ - elsif left[:type] == 'nonewline'
+ %td.old_line.diff-line-num
+ %td.line_content.parallel.match= left[:text]
+ %td.new_line.diff-line-num
+ %td.line_content.parallel.match= left[:text]
+ - else
+ %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]}"}
+ = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code]
- if @comments_allowed && can?(current_user, :create_note, @project)
- = link_to_new_diff_note(line_code_left, 'old')
- %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= raw line_content_left
+ = link_to_new_diff_note(left[:line_code], 'old')
+ %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text])
- - if type_right == 'new'
+ - if right[:type] == 'new'
- new_line_class = 'new'
- - new_line_code = line_code_right
+ - new_line_code = right[:line_code]
- else
- new_line_class = nil
- - new_line_code = line_code_left
+ - new_line_code = left[:line_code]
- %td.new_line{id: new_line_code, class: "#{new_line_class}", data: { linenumber: line_number_right }}
- = link_to raw(line_number_right), "##{new_line_code}", id: new_line_code
+ %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class}", data: { linenumber: right[:number] }}
+ = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code
- if @comments_allowed && can?(current_user, :create_note, @project)
- = link_to_new_diff_note(line_code_right, 'new')
- %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= raw line_content_right
+ = link_to_new_diff_note(right[:line_code], 'new')
+ %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { line_code: new_line_code }}= diff_line_content(right[:text])
- if @reply_allowed
- - comments_left, comments_right = organize_comments(type_left, type_right, line_code_left, line_code_right)
+ - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code])
- if comments_left.present? || comments_right.present?
= render "projects/notes/diff_notes_with_reply_parallel", notes_left: comments_left, notes_right: comments_right
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index 977ca423f75..9a8208202e4 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -3,9 +3,11 @@
.suppressed-container
%a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
-%table.text-file{class: "#{'hide' if too_big}"}
+%table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' }
+
- last_line = 0
- - diff_file.diff_lines.each_with_index do |line, index|
+ - raw_diff_lines = diff_file.diff_lines.to_a
+ - diff_file.highlighted_diff_lines.each_with_index do |line, index|
- type = line.type
- last_line = line.new_pos
- line_code = generate_line_code(diff_file.file_path, line)
@@ -14,23 +16,27 @@
- if type == "match"
= render "projects/diffs/match_line", {line: line.text,
line_old: line_old, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file}
+ - elsif type == 'nonewline'
+ %td.old_line.diff-line-num
+ %td.new_line.diff-line-num
+ %td.line_content.match= line.text
- else
- %td.old_line
+ %td.old_line.diff-line-num{class: type}
= link_to raw(type == "new" ? "&nbsp;" : line_old), "##{line_code}", id: line_code
- if @comments_allowed && can?(current_user, :create_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)
+ %td.new_line.diff-line-num{class: type, 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}", data: { line_code: line_code }}= diff_line_content(line.text)
- if @reply_allowed
- comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at)
- unless comments.empty?
- = render "projects/notes/diff_notes_with_reply", notes: comments, line: line.text
+ = render "projects/notes/diff_notes_with_reply", notes: comments, line: raw_diff_lines[index].text
- if last_line > 0
- = render "projects/diffs/match_line", {line: "",
- line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file}
+ = render "projects/diffs/match_line", { line: "",
+ line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file }
- if diff_file.diff.blank? && diff_file.mode_changed?
.file-mode-changed
diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml
index f99bc9a85eb..15536c17f8e 100644
--- a/app/views/projects/diffs/_warning.html.haml
+++ b/app/views/projects/diffs/_warning.html.haml
@@ -3,17 +3,16 @@
Too many changes to show.
.pull-right
- unless diff_hard_limit_enabled?
- = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: nil)), class: "btn btn-sm btn-warning"
+ = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: nil)), class: "btn btn-sm"
- 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-sm"
- = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-warning btn-sm"
+ = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-sm"
+ = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-sm"
- elsif @merge_request && @merge_request.persisted?
- = 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"
+ = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-sm"
+ = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-sm"
%p
To preserve performance only
- %strong #{shown_files_count} of #{diffs.size}
+ %strong #{diff_files.count} of #{diff_files.real_size}
files are displayed.
-
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 650629ef1b9..6d872cd0b21 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,6 +1,4 @@
-- @blank_container = true
-
-.project-edit-container
+.project-edit-container.prepend-top-default
.project-edit-errors
.project-edit-content
.panel.panel-default
@@ -86,6 +84,8 @@
%br
%span.descr Share code pastes with others out of git repository
+ = render 'builds_settings', f: f
+
%fieldset.features
%legend
Project avatar:
@@ -112,61 +112,6 @@
%hr
= link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
- %fieldset.features
- %legend
- Continuous Integration
- .form-group
- .col-sm-offset-2.col-sm-10
- %p Get recent application code using the following command:
- .radio
- = f.label :build_allow_git_fetch do
- = f.radio_button :build_allow_git_fetch, 'false'
- %strong git clone
- %br
- %span.descr Slower but makes sure you have a clean dir before every build
- .radio
- = f.label :build_allow_git_fetch do
- = f.radio_button :build_allow_git_fetch, 'true'
- %strong git fetch
- %br
- %span.descr Faster
- .form-group
- = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label'
- .col-sm-10
- = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
- %p.help-block per build in minutes
- .form-group
- = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label'
- .col-sm-10
- .input-group
- %span.input-group-addon /
- = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
- %span.input-group-addon /
- %p.help-block
- We will use this regular expression to find test coverage output in build trace.
- Leave blank if you want to disable this feature
- .bs-callout.bs-callout-info
- %p Below are examples of regex for existing tools:
- %ul
- %li
- Simplecov (Ruby) -
- %code \(\d+.\d+\%\) covered
- %li
- pytest-cov (Python) -
- %code \d+\%\s*$
- %li
- phpunit --coverage-text --colors=never (PHP) -
- %code ^\s*Lines:\s*\d+.\d+\%
-
-
- %fieldset.features
- %legend
- Advanced settings
- .form-group
- = f.label :runners_token, "CI token", class: 'control-label'
- .col-sm-10
- = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
- %p.help-block The secure token used to checkout project.
.form-actions
= f.submit 'Save changes', class: "btn btn-save"
@@ -174,6 +119,19 @@
.danger-settings
+ .panel.panel-default
+ .panel-heading Housekeeping
+ .errors-holder
+ .panel-body
+ %p
+ Runs a number of housekeeping tasks within the current repository,
+ such as compressing file revisions and removing unreachable objects.
+ %br
+
+ .form-actions
+ = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
+ method: :post, class: "btn btn-default"
+
- if can? current_user, :archive_project, @project
- if @project.archived?
.panel.panel-success
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 503d156661e..6ad7b05155a 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
= content_for :flash_message do
- if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh'
@@ -16,41 +18,42 @@
= link_to "adding README", new_readme_path, class: 'underlined-link'
file to this project.
-- if can?(current_user, :download_code, @project)
- .prepend-top-20
- .empty_wrapper
- %h3.page-title-empty
- Command line instructions
- %div.git-empty
- %fieldset
- %h5 Git global setup
- %pre.light-well
- :preserve
- git config --global user.name "#{h git_user_name}"
- git config --global user.email "#{h git_user_email}"
+- if can?(current_user, :push_code, @project)
+ %div{ class: container_class }
+ .prepend-top-20
+ .empty_wrapper
+ %h3.page-title-empty
+ Command line instructions
+ %div.git-empty
+ %fieldset
+ %h5 Git global setup
+ %pre.light-well
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
- %fieldset
- %h5 Create a new repository
- %pre.light-well
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- cd #{h @project.path}
- touch README.md
- git add README.md
- git commit -m "add README"
- git push -u origin master
+ %fieldset
+ %h5 Create a new repository
+ %pre.light-well
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ cd #{h @project.path}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ git push -u origin master
- %fieldset
- %h5 Existing folder or Git repository
- %pre.light-well
- :preserve
- cd existing_folder
- git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- git add .
- git commit
- git push -u origin master
+ %fieldset
+ %h5 Existing folder or Git repository
+ %pre.light-well
+ :preserve
+ cd existing_folder
+ git init
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git add .
+ git commit
+ git push -u origin master
- - if can? current_user, :remove_project, @project
- .prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+ - if can? current_user, :remove_project, @project
+ .prepend-top-20
+ = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
new file mode 100644
index 00000000000..905f6bbbd48
--- /dev/null
+++ b/app/views/projects/find_file/show.html.haml
@@ -0,0 +1,27 @@
+- page_title "Find File", @ref
+- header_title project_title(@project, "Files", project_files_path(@project))
+
+.file-finder-holder.tree-holder.clearfix
+ .gray-content-block.top-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'find_file', path: @path
+ %ul.breadcrumb.repo-breadcrumb
+ %li
+ = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
+ = @project.path
+ %li.file-finder
+ %input#file_find.form-control.file-finder-input{type: "text", placeholder: 'Find by path', autocomplete: 'off'}
+
+ %div.tree-content-holder
+ .table-holder
+ %table.table.files-slider{class: "table_#{@hex_path} tree-table table-striped" }
+ %tbody
+ = spinner nil, true
+
+:javascript
+ var projectFindFile = new ProjectFindFile($(".file-finder-holder"), {
+ url: "#{escape_javascript(namespace_project_files_path(@project.namespace, @project, @ref, @options.merge(format: :json)))}",
+ treeUrl: "#{escape_javascript(namespace_project_tree_path(@project.namespace, @project, @ref))}",
+ blobUrlTemplate: "#{escape_javascript(namespace_project_blob_path(@project.namespace, @project, @id || @commit.id))}"
+ });
+ new ShortcutsFindFile(projectFindFile);
diff --git a/app/views/projects/forks/_projects.html.haml b/app/views/projects/forks/_projects.html.haml
new file mode 100644
index 00000000000..2946e6dcbd0
--- /dev/null
+++ b/app/views/projects/forks/_projects.html.haml
@@ -0,0 +1,2 @@
+= render 'shared/projects/list', projects: projects, use_creator_avatar: true,
+ forks: true, show_last_commit_as_description: true
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
new file mode 100644
index 00000000000..4bcf2d9d533
--- /dev/null
+++ b/app/views/projects/forks/index.html.haml
@@ -0,0 +1,48 @@
+.top-area
+ .nav-text
+ - full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private"
+ == #{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
+
+ .nav-controls
+ = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = search_field_tag :filter_projects, nil, placeholder: 'Search forks', class: 'projects-list-filter project-filter-form-field form-control input-short',
+ spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
+
+ .dropdown
+ %button.dropdown-toggle.btn.sort-forks{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.dropdown-menu-align-right
+ %li
+ - excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
+ = link_to page_filter_path(sort: sort_value_recently_created, without: excluded_filters) do
+ = sort_title_recently_created
+ = link_to page_filter_path(sort: sort_value_oldest_created, without: excluded_filters) do
+ = sort_title_oldest_created
+ = link_to page_filter_path(sort: sort_value_recently_updated, without: excluded_filters) do
+ = sort_title_recently_updated
+ = link_to page_filter_path(sort: sort_value_oldest_updated, without: excluded_filters) do
+ = sort_title_oldest_updated
+
+ - if current_user && can?(current_user, :fork_project, @project)
+ - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
+ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do
+ = icon('code-fork fw')
+ Fork
+ - else
+ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-new' do
+ = icon('code-fork fw')
+ Fork
+
+
+= render 'projects', projects: @forks
+
+- if @private_forks_count > 0
+ .private-forks-notice
+ = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
+ %strong= pluralize(@private_forks_count, 'private fork')
+ %span you have no access to.
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index 8a2c027a455..edabc2d3b44 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -22,7 +22,7 @@
- else
.fork-thumbnail
- = link_to namespace_project_fork_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do
+ = link_to namespace_project_forks_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do
= image_tag namespace_icon(namespace, 100)
.caption
%strong
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
new file mode 100644
index 00000000000..c15386b4883
--- /dev/null
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -0,0 +1,58 @@
+%tr.generic_commit_status
+ %td.status
+ - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
+ = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url)
+ - else
+ = ci_status_with_icon(generic_commit_status.status)
+
+ %td.generic_commit_status-link
+ - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
+ = link_to generic_commit_status.target_url do
+ %strong ##{generic_commit_status.id}
+ - else
+ %strong ##{generic_commit_status.id}
+
+ - if defined?(commit_sha) && commit_sha
+ %td
+ = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
+
+ %td
+ - if generic_commit_status.ref
+ = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
+ - else
+ .light none
+
+ - if defined?(runner) && runner
+ %td
+ - if generic_commit_status.try(:runner)
+ = runner_link(generic_commit_status.runner)
+ - else
+ .light none
+
+ - if defined?(stage) && stage
+ %td
+ = generic_commit_status.stage
+
+ %td
+ = generic_commit_status.name
+
+ .pull-right
+ - if generic_commit_status.tags.any?
+ - generic_commit_status.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+
+ %td.duration
+ - if generic_commit_status.duration
+ #{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)}
+
+ %td.timestamp
+ - if generic_commit_status.finished_at
+ %span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
+
+ - if defined?(coverage) && coverage
+ %td.coverage
+ - if generic_commit_status.try(:coverage)
+ #{generic_commit_status.coverage}%
+
+ %td
diff --git a/app/views/projects/go_import.html.haml b/app/views/projects/go_import.html.haml
deleted file mode 100644
index 87ac75a350f..00000000000
--- a/app/views/projects/go_import.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-!!! 5
-%html
- %head
- - web_url = [Gitlab.config.gitlab.url, @namespace, @id].join('/')
- %meta{name: "go-import", content: "#{web_url.split('://')[1]} git #{web_url}.git"}
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index a47643bd09c..79a56647c53 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(action: :show) do
= link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
new file mode 100644
index 00000000000..13f5fc141fa
--- /dev/null
+++ b/app/views/projects/group_links/index.html.haml
@@ -0,0 +1,41 @@
+- page_title "Groups"
+%h3.page_title Share project with other groups
+%p.light
+ Projects can be stored in only one group at once. However you can share a project with other groups here.
+%hr
+- if @group_links.present?
+ .enabled-groups.panel.panel-default
+ .panel-heading
+ Already shared with
+ %ul.well-list
+ - @group_links.each do |group_link|
+ - group = group_link.group
+ %li
+ .pull-right
+ = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: 'btn btn-sm' do
+ %i.icon-remove
+ disable sharing
+ = link_to group do
+ %strong
+ %i.icon-folder-open
+ = group.name
+ %br
+ .light up to #{group_link.human_access}
+
+
+.available-groups
+ %h4
+ Can be shared with
+ %div
+ = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post, class: 'form-horizontal' do
+ .form-group
+ = label_tag :link_group_id, 'Group', class: 'control-label'
+ .col-sm-10
+ = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path))
+ .form-group
+ = label_tag :link_group_access, 'Max access level', class: 'control-label'
+ .col-sm-10
+ = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control"
+ .form-actions
+ = submit_tag "Share", class: "btn btn-create"
+
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index b18d9197d0b..67d016bd871 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -1,9 +1,9 @@
-- page_title "Web Hooks"
+- page_title "Webhooks"
%h3.page-title
- Web hooks
+ Webhooks
%p.light
- #{link_to "Web hooks ", help_page_path("web_hooks", "web_hooks"), class: "vlink"} can be
+ #{link_to "Webhooks ", help_page_path("web_hooks", "web_hooks"), class: "vlink"} can be
used for binding events when something is happening within the project.
%hr.clearfix
@@ -47,14 +47,14 @@
= f.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
- This url will be triggered when an issue is created
+ This url will be triggered when an issue is created/updated/merged
%div
= f.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
- This url will be triggered when a merge request is created
+ This url will be triggered when a merge request is created/updated/merged
%div
= f.check_box :build_events, class: 'pull-left'
.prepend-left-20
@@ -70,12 +70,12 @@
= f.check_box :enable_ssl_verification
%strong Enable SSL verification
.form-actions
- = f.submit "Add Web Hook", class: "btn btn-create"
+ = f.submit "Add Webhook", class: "btn btn-create"
-if @hooks.any?
.panel.panel-default
.panel-heading
- Web hooks (#{@hooks.count})
+ Webhooks (#{@hooks.count})
%ul.well-list
- @hooks.each do |hook|
%li
diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml
index de415ae51a4..38469ed4774 100644
--- a/app/views/projects/issues/_closed_by_box.html.haml
+++ b/app/views/projects/issues/_closed_by_box.html.haml
@@ -1,2 +1,4 @@
-.issue-closed-by-widget.gray-content-block.second-block.white
- This issue will be closed automatically when merge request #{markdown(merge_requests_sentence(@closed_by_merge_requests), pipeline: :gfm)} is accepted.
+.issue-closed-by-widget.second-block
+ - pluralized_mr_this = merge_request_count > 1 ? "these" : "this"
+ - pluralized_mr_is = merge_request_count > 1 ? "are" : "is"
+ When #{pluralized_mr_this} merge #{"request".pluralize(merge_request_count)} #{pluralized_mr_is} accepted, this issue will be closed automatically.
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index dc434cf38c4..b151393abab 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,9 +1,7 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- - if @issue.closed?
- = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue'
- - else
- = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-close js-note-target-close', title: 'Close Issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'projects/notes/notes_with_form'
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 6588d9bdbe1..33c48199ba5 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-requires-input' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-quick-submit js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
:javascript
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index f9cf4910df3..00e1a3d8069 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -5,7 +5,8 @@
.issue-title
%span.issue-title-text
- = link_to_gfm issue.title, issue_path(issue), class: "row_title"
+ = confidential_icon(issue)
+ = link_to_gfm issue.title, issue_path(issue), class: "title"
%ul.controls.light
- if issue.closed?
%li
@@ -15,6 +16,17 @@
%li
= link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
+ - upvotes, downvotes = issue.upvotes, issue.downvotes
+ - if upvotes > 0
+ %li
+ = icon('thumbs-up')
+ = upvotes
+
+ - if downvotes > 0
+ %li
+ = icon('thumbs-down')
+ = downvotes
+
- note_count = issue.notes.user.count
- if note_count > 0
%li
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index ca5b1a8386d..f34f3c05737 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -5,9 +5,4 @@
.nothing-here-block No issues to show
- if @issues.present?
- .issuable-filter-count
- %span.pull-right
- = @issues.total_count
- issues for this filter
-
= paginate @issues, theme: "gitlab"
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 254968e4f67..d6b38b327ff 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -1,7 +1,7 @@
--if @merge_requests.any?
+- if @merge_requests.any?
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
- %ul.bordered-list
+ %ul.unstyled-list
- has_any_ci = @merge_requests.any?(&:ci_commit)
- @merge_requests.each do |merge_request|
%li
@@ -11,7 +11,7 @@
- elsif has_any_ci
= icon('blank fw')
%span.merge-request-id
- \##{merge_request.iid}
+ = merge_request.to_reference
%span.merge-request-info
%strong
= link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
@@ -24,3 +24,5 @@
MERGED
- elsif merge_request.closed?
CLOSED
+ - if @closed_by_merge_requests.present?
+ = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
new file mode 100644
index 00000000000..e66e4669d48
--- /dev/null
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -0,0 +1,5 @@
+- if current_user && can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
+ .pull-right
+ = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn', title: @issue.to_branch_name do
+ = icon('code-fork')
+ New Branch
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
new file mode 100644
index 00000000000..b10cd03515f
--- /dev/null
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -0,0 +1,15 @@
+- if @related_branches.any?
+ %h2.related-branches-title
+ = pluralize(@related_branches.count, 'Related Branch')
+ %ul.unstyled-list
+ - @related_branches.each do |branch|
+ %li
+ - sha = @project.repository.find_branch(branch).target
+ - ci_commit = @project.ci_commit(sha) if sha
+ - if ci_commit
+ %span.related-branch-ci-status
+ = render_ci_status(ci_commit)
+ %span.related-branch-info
+ %strong
+ = link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do
+ = branch
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index dc8e477185b..ee8a9414657 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_issues_url(@project.namespace, @project)
- xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.any?
@issues.each do |issue|
issue_to_atom(xml, issue)
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index d6260ab2900..fde9304c0f8 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -5,22 +5,19 @@
- 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")
-.project-issuable-filter
- .controls
- .pull-left
- - if current_user
- .hidden-xs.pull-left
- = 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
-
+.top-area
+ = render 'shared/issuable/nav', type: :issues
+ .nav-controls
+ - if current_user
+ = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do
+ = icon('rss')
= render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
-
- if can? current_user, :create_issue, @project
- = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do
- %i.fa.fa-plus
+ = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
+ = icon('plus')
New Issue
- = render 'shared/issuable/filter', type: :issues
+= render 'shared/issuable/filter', type: :issues
.issues-holder
= render "issues"
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index f931a0d3b92..ce5b84ee712 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -5,37 +5,54 @@
= render "header_title"
.issue
- .detail-page-header
- .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} Closed
- .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} Open
- %span.identifier
- Issue ##{@issue.iid}
- %span.creator
- &middot;
- opened by #{link_to_member(@project, @issue.author, size: 24)}
- &middot;
- = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago')
- - if @issue.updated_at != @issue.created_at
- %span
- &middot;
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago')
+ .detail-page-header.issuable-header
+ .pull-left
+ .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"}
+ %span.hidden-xs
+ Closed
+ %span.hidden-sm.hidden-md.hidden-lg
+ = icon('check')
+ .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"}
+ %span.hidden-xs
+ Open
+ %span.hidden-sm.hidden-md.hidden-lg
+ = icon('circle-o')
- .pull-right
+ %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+
+ .issue-meta
+ = confidential_icon(@issue)
+ %strong.identifier
+ Issue ##{@issue.iid}
+ %span.creator
+ opened
+ .editor-details
+ .editor-details
+ = time_ago_with_tooltip(@issue.created_at)
+ by
+ %strong
+ = link_to_member(@project, @issue.author, avatar: false, size: 24, mobile_classes: "hidden-xs")
+ %strong
+ = link_to_member(@project, @issue.author, avatar: false, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
+ by_username: true, avatar: false)
+
+ .pull-right.issue-btn-group
- if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New Issue', id: 'new_issue_link' do
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
= icon('plus')
- New Issue
+ New issue
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
- = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do
= icon('pencil-square-o')
Edit
+
.issue-details.issuable-details
- .detail-page-description.gray-content-block.second-block
+ .detail-page-description.content-block
%h2.title
= markdown escape_once(@issue.title), pipeline: :single_line
%div
@@ -46,22 +63,19 @@
= markdown(@issue.description, cache_key: [@issue, "description"])
%textarea.hidden.js-task-list-field
= @issue.description
+ = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
- .merge-requests
- = render 'merge_requests'
+ .merge-requests
+ = render 'merge_requests'
+ = render 'related_branches'
- .gray-content-block.second-block.oneline-block
+ .content-block.content-block-small
+ = render 'new_branch'
= render 'votes/votes_block', votable: @issue
- - if @closed_by_merge_requests.present?
- = render 'projects/issues/closed_by_box'
-
.row
- %section.col-md-9
+ %section.col-md-12
.issuable-discussion
= render 'projects/issues/discussion'
- %aside.col-md-3
- = render 'shared/issuable/sidebar', issuable: @issue
-
- = render 'shared/show_aside'
+= render 'shared/issuable/sidebar', issuable: @issue
diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml
index 2f0f3fcfb06..986d8c220db 100644
--- a/app/views/projects/issues/update.js.haml
+++ b/app/views/projects/issues/update.js.haml
@@ -1,3 +1,3 @@
-$('.issuable-sidebar').html("#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}");
-$('.issuable-sidebar').parent().effect('highlight')
-new Issue();
+$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}";
+$('aside.right-sidebar').effect('highlight');
+new IssuableContext();
diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml
index 5ce2a7b985d..be7a0bb5628 100644
--- a/app/views/projects/labels/_form.html.haml
+++ b/app/views/projects/labels/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
-if @label.errors.any?
.row
.col-sm-offset-2.col-sm-10
@@ -10,7 +10,11 @@
.form-group
= f.label :title, class: 'control-label'
.col-sm-10
- = f.text_field :title, class: "form-control js-quick-submit", required: true, autofocus: true
+ = f.text_field :title, class: "form-control", required: true, autofocus: true
+ .form-group
+ = f.label :description, class: 'control-label'
+ .col-sm-10
+ = f.text_field :description, class: "form-control js-quick-submit"
.form-group
= f.label :color, "Background color", class: 'control-label'
.col-sm-10
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index b70a9fc9fe5..4927d239c1e 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -1,10 +1,25 @@
%li{id: dom_id(label)}
- = link_to_label(label)
+ = render "shared/label_row", label: label
+
.pull-right
%strong.append-right-20
+ = link_to_label(label, type: :merge_request) do
+ = pluralize label.open_merge_requests_count, 'open merge request'
+
+ %strong.append-right-20
= link_to_label(label) do
= pluralize label.open_issues_count, 'open issue'
+ - if current_user
+ .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
+ .subscription-status{data: {status: label_subscription_status(label)}}
+ %button.btn.btn-sm.btn-info.subscribe-button
+ %span= label_subscription_toggle_button_text(label)
+
- if can? current_user, :admin_label, @project
= link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm'
= link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
+
+- if current_user
+ :javascript
+ new Subscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 9081bcfe9b3..cc41130a9dc 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,13 +1,14 @@
- page_title "Labels"
= render "header_title"
-.gray-content-block.top-block
- - if can? current_user, :admin_label, @project
- = link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do
- = icon('plus')
- New label
- .oneline
+.top-area
+ .nav-text
Labels can be applied to issues and merge requests.
+ .nav-controls
+ - if can? current_user, :admin_label, @project
+ = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do
+ = icon('plus')
+ New label
.labels
- if @labels.present?
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index bff3c3b283d..393998f15b9 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -1,8 +1,8 @@
- content_for :note_actions do
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
- = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request"
+ = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- if @merge_request.closed?
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request"
+ = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index a051729dc32..18cf3f14f0b 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,7 +1,7 @@
%li{ class: mr_css_classes(merge_request) }
.merge-request-title
%span.merge-request-title-text
- = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
+ = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "title"
%ul.controls.light
- if merge_request.merged?
%li
@@ -24,6 +24,17 @@
%li
= link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
+ - upvotes, downvotes = merge_request.upvotes, merge_request.downvotes
+ - if upvotes > 0
+ %li
+ = icon('thumbs-up')
+ = upvotes
+
+ - if downvotes > 0
+ %li
+ = icon('thumbs-down')
+ = downvotes
+
- note_count = merge_request.mr_and_commit_notes.user.count
- if note_count > 0
%li
@@ -37,7 +48,7 @@
= note_count
.merge-request-info
- \##{merge_request.iid} &middot;
+ #{merge_request.to_reference} &middot;
opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
by #{link_to_member(@project, merge_request.author, avatar: false)}
- if merge_request.target_project.default_branch != merge_request.target_branch
@@ -53,7 +64,7 @@
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
- = link_to_label(label, project: merge_request.project)
+ = link_to_label(label, project: merge_request.project, type: 'merge_request')
- if merge_request.tasks?
&nbsp;
%span.task-status
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index 0af970e4b92..5473fa19166 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -5,10 +5,5 @@
.nothing-here-block No merge requests to show
- if @merge_requests.present?
- .issuable-filter-count
- %span.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 236a545c840..01dc7519bee 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -33,23 +33,18 @@
%div= msg
- elsif @merge_request.source_branch.present? && @merge_request.target_branch.present?
- - if @merge_request.compare_failed
- .alert.alert-danger
- %h4 Compare failed
- %p We can't compare selected branches. It may be because of huge diff. Please try again or select different branches.
- - else
- .light-well.append-bottom-default
- .center
- %h4
- There isn't anything to merge.
- %p.slead
- - if @merge_request.source_branch == @merge_request.target_branch
- You'll need to use different branch names to get a valid comparison.
- - else
- %span.label-branch #{@merge_request.source_branch}
- and
- %span.label-branch #{@merge_request.target_branch}
- are the same.
+ .light-well.append-bottom-default
+ .center
+ %h4
+ There isn't anything to merge.
+ %p.slead
+ - if @merge_request.source_branch == @merge_request.target_branch
+ You'll need to use different branch names to get a valid comparison.
+ - else
+ %span.label-branch #{@merge_request.source_branch}
+ and
+ %span.label-branch #{@merge_request.target_branch}
+ are the same.
.form-actions
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index a14943b15d3..9e59f7df71b 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -18,7 +18,7 @@
= f.hidden_field :target_branch
.mr-compare.merge-request
- %ul.merge-request-tabs.center-top-menu.no-top.no-bottom
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab
= link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits
@@ -31,22 +31,18 @@
%li.diffs-tab.active
= link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
Changes
- %span.badge= @diffs.size
+ %span.badge= @diffs.real_size
.tab-content
#commits.commits.tab-pane
= render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane.active
- - if @diffs.present?
- = render "projects/diffs/diffs", diffs: @diffs, project: @project
- - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
+ - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
.alert.alert-danger
%h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits.
%p To preserve performance the line changes are not shown.
- else
- .alert.alert-danger
- %h4 This comparison includes a huge diff.
- %p To preserve performance the line changes are not shown.
+ = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs
- if @ci_commit
#builds.builds.tab-pane
= render "projects/merge_requests/show/builds"
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index ba7c2c01e93..ee5b9fd95a8 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,4 +1,4 @@
-- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
@@ -34,18 +34,20 @@
%span into
= link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do
= @merge_request.target_branch
+ - if @merge_request.open? && @merge_request.diverged_from_target_branch?
+ %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
= render "projects/merge_requests/show/how_to_merge"
= render "projects/merge_requests/widget/show.html.haml"
- - if @merge_request.open? && @merge_request.source_branch_exists? && @merge_request.can_be_merged? && @merge_request.can_be_merged_by?(current_user)
+ - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
.light.prepend-top-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- if @commits.present?
- %ul.merge-request-tabs.center-top-menu.no-top.no-bottom
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion
@@ -62,20 +64,17 @@
%li.diffs-tab
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
Changes
- %span.badge= @merge_request.diffs.size
+ %span.badge= @merge_request.diff_size
.tab-content
#notes.notes.tab-pane.voting_notes
- .gray-content-block.second-block.oneline-block
+ .content-block.content-block-small.oneline-block
= render 'votes/votes_block', votable: @merge_request
.row
- %section.col-md-9
+ %section.col-md-12
.issuable-discussion
= render "projects/merge_requests/discussion"
- %aside.col-md-3
- = render 'shared/issuable/sidebar', issuable: @merge_request
- = render 'shared/show_aside'
#commits.commits.tab-pane
- # This tab is always loaded via AJAX
@@ -87,6 +86,10 @@
.mr-loading-status
= spinner
+= render 'shared/issuable/sidebar', issuable: @merge_request
+- if @merge_request.can_be_reverted?
+ = render "projects/commit/revert", commit: @merge_request.merge_commit, title: @merge_request.title
+
:javascript
var merge_request;
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 086298e5af1..e56a44e0a79 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -2,15 +2,19 @@
= render "header_title"
= render 'projects/last_push'
-.project-issuable-filter
- .controls
+
+.top-area
+ = render 'shared/issuable/nav', type: :merge_requests
+ .nav-controls
= render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
- - if can? current_user, :create_merge_request, @project
- .pull-left.hidden-xs
- = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new", title: "New Merge Request" do
- %i.fa.fa-plus
- New Merge Request
- = render 'shared/issuable/filter', type: :merge_requests
+ - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - if merge_project
+ = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
+ = icon('plus')
+ New Merge Request
+
+= render 'shared/issuable/filter', type: :merge_requests
+
.merge-requests-holder
= render 'merge_requests'
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index 7f904ec42a0..a8f09f855d4 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -1,4 +1,4 @@
-.gray-content-block.middle-block.oneline-block
+.content-block.oneline-block
= icon("sort-amount-desc")
Most recent commits displayed first
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index d9cfc3d7ae9..1b0bae86ad4 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,5 +1,6 @@
- if @merge_request_diff.collected?
- = render "projects/diffs/diffs", diffs: params[:w] == '1' ? @merge_request.diffs_no_whitespace : @merge_request.diffs, project: @merge_request.project
+ = render "projects/diffs/diffs", diffs: @merge_request.diffs(diff_options),
+ project: @merge_request.project, diff_refs: @merge_request.diff_refs
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
- else
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index 877cc3d744b..0dbd159298e 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
@@ -45,6 +45,10 @@
- unless @merge_request.can_be_merged_by?(current_user)
%p
Note that pushing to GitLab requires write access to this repository.
+ %p
+ %strong Tip:
+ You can also checkout merge requests locally by
+ %a{href: 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/workflow/merge_requests.md#checkout-merge-requests-locally', target: '_blank'} following these guidelines
:javascript
$(function(){
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 0f81e5e8914..a23bd8d18d0 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -1,4 +1,4 @@
-.detail-page-description.gray-content-block.second-block
+.detail-page-description.content-block
%h2.title
= markdown escape_once(@merge_request.title), pipeline: :single_line
@@ -10,3 +10,5 @@
= markdown(@merge_request.description, cache_key: [@merge_request, "description"])
%textarea.hidden.js-task-list-field
= @merge_request.description
+
+ = edited_time_ago_with_tooltip(@merge_request, placement: 'bottom')
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index fc6fb2a0d42..c6cbe8589ef 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -1,18 +1,28 @@
.detail-page-header
.status-box{ class: status_box_class(@merge_request) }
- = @merge_request.state_human_name
- %span.identifier
- Merge Request ##{@merge_request.iid}
- %span.creator
- &middot;
- opened by #{link_to_member(@project, @merge_request.author, size: 24)}
- &middot;
- = time_ago_with_tooltip(@merge_request.created_at)
- - if @merge_request.updated_at != @merge_request.created_at
- %span
- &middot;
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(@merge_request.updated_at, placement: 'bottom')
+ %span.hidden-xs
+ = @merge_request.state_human_name
+ %span.hidden-sm.hidden-md.hidden-lg
+ = icon(@merge_request.state_icon_name)
+ %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+ .issue-meta
+ %strong.identifier
+ %span.hidden-sm.hidden-md.hidden-lg
+ MR
+ %span.hidden-xs
+ Merge Request
+ !#{@merge_request.iid}
+ %span.creator
+ opened
+ .editor-details
+ = time_ago_with_tooltip(@merge_request.created_at)
+ by
+ %strong
+ = link_to_member(@project, @merge_request.author, avatar: false, size: 24, mobile_classes: "hidden-xs")
+ %strong
+ = link_to_member(@project, @merge_request.author, avatar: false, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
+ by_username: true, avatar: false)
.issue-btn-group.pull-right
- if can?(current_user, :update_merge_request, @merge_request)
diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml
index 93db65ddf79..9cce5660e1c 100644
--- a/app/views/projects/merge_requests/update.js.haml
+++ b/app/views/projects/merge_requests/update.js.haml
@@ -1,3 +1,3 @@
-$('.issuable-sidebar').html("#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}");
-$('.issuable-sidebar').parent().effect('highlight')
-merge_request = new MergeRequest();
+$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}";
+$('aside.right-sidebar').effect('highlight');
+new IssuableContext();
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index d1d602eecdc..3abae9f0bf6 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -8,20 +8,18 @@
#{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
%div
- if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true')
- The changes were merged into
- #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
- The source branch has been removed.
-
+ %p
+ The changes were merged into
+ #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
+ The source branch has been removed.
+ = render 'projects/merge_requests/widget/merged_buttons'
- elsif @merge_request.can_remove_source_branch?(current_user)
.remove_source_branch_widget
%p
The changes were merged into
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
You can remove the source branch now.
- = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do
- %i.fa.fa-times
- Remove Source Branch
-
+ = render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true
.remove_source_branch_widget.failed.hide
%p
Failed to remove source branch '#{@merge_request.source_branch}'.
diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml
new file mode 100644
index 00000000000..85a3a6ba9e2
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml
@@ -0,0 +1,11 @@
+- source_branch_exists = local_assigns.fetch(:source_branch_exists, false)
+- mr_can_be_reverted = @merge_request.can_be_reverted?
+
+- if source_branch_exists || mr_can_be_reverted
+ .btn-group
+ - if source_branch_exists
+ = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-grouped btn-sm remove_source_branch" do
+ = icon('trash-o')
+ Remove Source Branch
+ - if mr_can_be_reverted
+ = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index d9a1730a8bc..807833741af 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -1,6 +1,6 @@
- status_class = @ci_commit ? " ci-#{@ci_commit.status}" : nil
-= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f|
+= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
= hidden_field_tag :authenticity_token, form_authenticity_token
.accept-merge-holder.clearfix.js-toggle-container
.clearfix
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 39aa2437e18..23f2bca7baf 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-requires-input'} do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f|
-if @milestone.errors.any?
.alert.alert-danger
%ul
@@ -9,12 +9,12 @@
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
- = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true
+ = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
- = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
+ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
= render 'projects/notes/hints'
.clearfix
.error-alert
diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml
deleted file mode 100644
index 133d802aaca..00000000000
--- a/app/views/projects/milestones/_issue.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) }
- .pull-right.assignee-icon
- - if issue.assignee
- = image_tag avatar_icon(issue.assignee, 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
-
diff --git a/app/views/projects/milestones/_issues.html.haml b/app/views/projects/milestones/_issues.html.haml
deleted file mode 100644
index 6e4df75a3df..00000000000
--- a/app/views/projects/milestones/_issues.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list issues-sortable-list", id: "issues-list-#{id}", "data-state" => id }
- - issues.sort_by(&:position).each do |issue|
- = render 'issue', issue: issue
- %li.light.ui-sort-disabled Drag and drop available
diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml
deleted file mode 100644
index a1033607c5d..00000000000
--- a/app/views/projects/milestones/_merge_request.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => merge_request_path(merge_request) }
- %span.str-truncated
- = link_to [@project.namespace.becomes(Namespace), @project, merge_request] do
- %span.cgray ##{merge_request.iid}
- = 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, 16), class: "avatar s16", alt: ''
diff --git a/app/views/projects/milestones/_merge_requests.html.haml b/app/views/projects/milestones/_merge_requests.html.haml
deleted file mode 100644
index 00889a5eb24..00000000000
--- a/app/views/projects/milestones/_merge_requests.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list merge_requests-sortable-list", id: "merge_requests-list-#{id}", "data-state" => id }
- - merge_requests.sort_by(&:position).each do |merge_request|
- = render 'merge_request', merge_request: merge_request
- %li.light.ui-sort-disabled Drag and drop available
diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml
index d6a44c9f0a1..77b566db6b6 100644
--- a/app/views/projects/milestones/_milestone.html.haml
+++ b/app/views/projects/milestones/_milestone.html.haml
@@ -1,30 +1,5 @@
-%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) }
- .row
- .col-sm-6
- %strong
- = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
-
- .col-sm-6
- .pull-right.light #{milestone.percent_complete}% complete
- .row
- .col-sm-6
- = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do
- = pluralize milestone.issues.count, 'Issue'
- &middot;
- = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do
- = pluralize milestone.merge_requests.count, 'Merge Request'
- .col-sm-6
- = milestone_progress_bar(milestone)
-
- .row
- .col-sm-6
- = render 'shared/milestone_expired', milestone: milestone
- .col-sm-6
- - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
- = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs 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-xs btn-close"
- = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
- %i.fa.fa-trash-o
- Delete
+= render 'shared/milestones/milestone',
+ milestone_path: namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone),
+ issues_path: namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title),
+ merge_requests_path: namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title),
+ milestone: milestone
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 114b06457a5..abe567af1dd 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -2,17 +2,14 @@
= render "header_title"
-.project-issuable-filter
- .controls
- - if can?(current_user, :admin_milestone, @project)
- = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do
- %i.fa.fa-plus
- New Milestone
-
+.top-area
= render 'shared/milestones_filter'
-.gray-content-block
- Milestone allows you to group issues and set due date for it
+ .nav-controls
+ - if can?(current_user, :admin_milestone, @project)
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do
+ = icon('plus')
+ New Milestone
.milestones
%ul.content-list
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 1670ea8741a..be63875ab34 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -20,19 +20,19 @@
.pull-right
- if can?(current_user, :admin_milestone, @project)
- if @milestone.active?
- = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped"
+ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
- else
- = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped"
+ = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
- = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do
- %i.fa.fa-trash-o
+ = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-nr" do
+ = icon('trash-o')
Delete
- = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do
- %i.fa.fa-pencil-square-o
+ = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do
+ = icon('pencil-square-o')
Edit
-.detail-page-description.gray-content-block.second-block
+.detail-page-description.milestone-detail.second-block
%h2.title
= markdown escape_once(@milestone.title), pipeline: :single_line
%div
@@ -42,90 +42,9 @@
= preserve do
= markdown @milestone.description
-- if @milestone.issues.any? && @milestone.can_be_closed?
+- if @milestone.complete?(current_user) && @milestone.active?
.alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close milestone now.
-.context.prepend-top-default
- %p.lead
- Progress:
- #{@milestone.closed_items_count} closed
- &ndash;
- #{@milestone.open_items_count} open
- &nbsp;
- %span.light #{@milestone.percent_complete}% complete
- %span.pull-right= @milestone.expires_at
- = milestone_progress_bar(@milestone)
-
-%ul.center-top-menu.no-top.no-bottom
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab' do
- Issues
- %span.badge= @issues.count
- %li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
- Merge Requests
- %span.badge= @merge_requests.count
- %li
- = link_to '#tab-participants', 'data-toggle' => 'tab' do
- Participants
- %span.badge= @users.count
-
-.tab-content
- .tab-pane.active#tab-issues
- .gray-content-block.middle-block
- .pull-right
- - if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
- %i.fa.fa-plus
- New Issue
- - if can?(current_user, :read_issue, @project)
- = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All issues in this milestone
-
- .row.prepend-top-default
- .col-md-4
- = render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned, id: 'unassigned')
- .col-md-4
- = render('issues', title: 'Ongoing Issues (open and assigned)', issues: @issues.opened.assigned, id: 'ongoing')
- .col-md-4
- = render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed')
-
- .tab-pane#tab-merge-requests
- .gray-content-block.middle-block
- .pull-right
- - if can?(current_user, :read_merge_request, @project)
- = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All merge requests in this milestone
-
- .row.prepend-top-default
- .col-md-3
- = render('merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: @merge_requests.opened.unassigned, id: 'unassigned')
- .col-md-3
- = render('merge_requests', title: 'Waiting for merge (open and assigned)', merge_requests: @merge_requests.opened.assigned, id: 'ongoing')
- .col-md-3
- = render('merge_requests', title: 'Rejected (closed)', merge_requests: @merge_requests.closed, id: 'closed')
- .col-md-3
- .panel.panel-primary
- .panel-heading Merged
- %ul.well-list
- - @merge_requests.merged.each do |merge_request|
- = render 'merge_request', merge_request: merge_request
-
- .tab-pane#tab-participants
- .gray-content-block.middle-block
- .oneline
- All participants to this milestone
-
- %ul.bordered-list
- - @users.each do |user|
- %li
- = link_to user, title: user.name, class: "darken" do
- = image_tag avatar_icon(user, 32), class: "avatar s32"
- %strong= truncate(user.name, lenght: 40)
- %br
- %small.cgray= user.username
+= render 'shared/milestones/summary', milestone: @milestone, project: @project
+= render 'shared/milestones/tabs', milestone: @milestone
diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml
index c731baf0a65..11f9859a90f 100644
--- a/app/views/projects/notes/_diff_notes_with_reply.html.haml
+++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml
@@ -7,7 +7,7 @@
%i.fa.fa-comment
= notes.count
%td.notes_content
- %ul.notes{ rel: note.discussion_id }
+ %ul.notes{ data: { discussion_id: note.discussion_id } }
= render notes
.discussion-reply-holder
= link_to_reply_diff(note)
diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml
index c6726cbafa3..bb761ed2f94 100644
--- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml
+++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml
@@ -8,7 +8,7 @@
%i.fa.fa-comment
= notes_left.count
%td.notes_content.parallel.old
- %ul.notes{ rel: note1.discussion_id }
+ %ul.notes{ data: { discussion_id: note1.discussion_id } }
= render notes_left
.discussion-reply-holder
@@ -23,7 +23,7 @@
%i.fa.fa-comment
= notes_right.count
%td.notes_content.parallel.new
- %ul.notes{ rel: note2.discussion_id }
+ %ul.notes{ data: { discussion_id: note2.discussion_id } }
= render notes_right
.discussion-reply-holder
@@ -31,4 +31,3 @@
- else
%td.notes_line.new= ""
%td.notes_content.parallel.new= ""
-
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 3ccda1b381c..13e624764d9 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -1,10 +1,10 @@
.note-edit-form
- = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f|
+ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f|
= note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field js-quick-submit'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
= render 'projects/notes/hints'
.note-form-actions
- = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button'
- = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel'
+ = f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button'
+ = link_to 'Cancel', '#', class: 'btn btn-nr btn-cancel note-edit-cancel'
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index acb6dc52a8e..f675f092da1 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= note_target_fields(@note)
@@ -8,11 +8,12 @@
= f.hidden_field :noteable_type
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-quick-submit'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text'
= render 'projects/notes/hints'
.error-alert
.note-form-actions.clearfix
- = f.submit 'Add Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button"
+ = f.submit 'Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button"
= yield(:note_actions)
- %a.btn.btn-cancel.js-close-discussion-note-form Cancel
+ %a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}}
+ Discard draft
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 922535e5c4a..2cf32e6093d 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -1,4 +1,4 @@
-%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: { discussion: note.discussion_id } }
+%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)] }
.timeline-entry-inner
.timeline-icon
%a{href: user_path(note.author)}
@@ -27,20 +27,13 @@
%span.note-last-update
%a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'}
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago')
- - if note.updated_at != note.created_at
- %span
- &middot;
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago')
- - if note.updated_by && note.updated_by != note.author
- by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)}
-
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
= preserve do
= markdown(note.note, pipeline: :note, cache_key: [note, "note"])
- if note_editable?(note)
= render 'projects/notes/edit_form', note: note
+ = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note.attachment.url
.note-attachment
@@ -54,4 +47,3 @@
= link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.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
= icon('trash-o', class: 'cred')
- .clear
diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml
index ca60dd239b2..62db86fb181 100644
--- a/app/views/projects/notes/_notes.html.haml
+++ b/app/views/projects/notes/_notes.html.haml
@@ -2,10 +2,14 @@
- @discussions.each do |discussion_notes|
- note = discussion_notes.first
- if note_for_main_target?(note)
+ - next if note.cross_reference_not_visible_for?(current_user)
+
= render discussion_notes
- else
= render 'projects/notes/discussion', discussion_notes: discussion_notes
- else
- @notes.each do |note|
- next unless note.author
+ - next if note.cross_reference_not_visible_for?(current_user)
+
= render note
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index eb378b42603..910eb6cf66e 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -5,6 +5,16 @@
.js-main-target-form
- if can? current_user, :create_note, @project
= render "projects/notes/form", view: diff_view
+- else
+ .disabled-comment-area
+ .disabled-profile
+ .disabled-comment
+ %span
+ Please
+ = link_to "register",new_user_session_path
+ or
+ = link_to "login",new_user_session_path
+ to post a comment
:javascript
var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml
index 6903fad4a0a..3da2f2060b8 100644
--- a/app/views/projects/notes/discussions/_commit.html.haml
+++ b/app/views/projects/notes/discussions/_commit.html.haml
@@ -20,8 +20,7 @@
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
- else
.panel.panel-default
- .notes{ rel: discussion_notes.first.discussion_id }
+ .notes{ data: { discussion_id: discussion_notes.first.discussion_id } }
= render discussion_notes
.discussion-reply-holder
= link_to_reply_diff(discussion_notes.first)
-
diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml
index 0301445b5b2..820e31ccd61 100644
--- a/app/views/projects/notes/discussions/_diff.html.haml
+++ b/app/views/projects/notes/discussions/_diff.html.haml
@@ -9,22 +9,22 @@
= 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
+ .diff-content.code.js-syntax-highlight
%table
- note.truncated_diff_lines.each do |line|
- type = line.type
- line_code = generate_line_code(note.file_path, line)
%tr.line_holder{ id: line_code, class: "#{type}" }
- if type == "match"
- %td.old_line= "..."
- %td.new_line= "..."
- %td.line_content.matched= line.text
+ %td.old_line.diff-line-num= "..."
+ %td.new_line.diff-line-num= "..."
+ %td.line_content.match= line.text
- else
- %td.old_line
+ %td.old_line.diff-line-num
= raw(type == "new" ? "&nbsp;" : line.old_pos)
- %td.new_line
+ %td.new_line.diff-line-num
= raw(type == "old" ? "&nbsp;" : line.new_pos)
- %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text)
+ %td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text)
- if line_code == note.line_code
= render "projects/notes/diff_notes_with_reply", notes: discussion_notes
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
index 1c2458fa144..c53033e367c 100644
--- a/app/views/projects/project_members/_group_members.html.haml
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -5,7 +5,7 @@
%small
(#{members.count})
- if can?(current_user, :admin_group_member, @group)
- .pull-right
+ .controls
= link_to group_group_members_path(@group), class: 'btn' do
= icon('pencil-square-o')
Manage group members
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index d708b01a114..f0f3bb3c177 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -4,7 +4,7 @@
.col-sm-10
= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
.help-block
- Search for existing users or invite new ones using their email address.
+ Search for users by name, username, or email, or invite new ones using their email address.
.form-group
= f.label :access_level, "Project Access", class: 'control-label'
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
new file mode 100644
index 00000000000..62888e41935
--- /dev/null
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -0,0 +1,21 @@
+- @project_group_links.each do |group_links|
+ - shared_group = group_links.group
+ - shared_group_users_count = group_links.group.group_members.count
+ .panel.panel-default
+ .panel-heading
+ Shared with
+ %strong #{shared_group.name}
+ group, members with
+ %strong #{group_links.human_access}
+ role (#{shared_group_users_count})
+ - if current_user.can?(:admin_group, shared_group)
+ .panel-head-actions
+ = link_to group_group_members_path(shared_group), class: 'btn btn-sm' do
+ %i.fa.fa-pencil-square-o
+ Edit group members
+ %ul.content-list
+ - shared_group.group_members.order('access_level DESC').limit(20).each do |member|
+ = render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false
+ - if shared_group_users_count > 20
+ %li
+ and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)}
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index ccddab13aaf..e8dce30425f 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -4,7 +4,7 @@
project members
%small
(#{members.count})
- .pull-right
+ .controls
= form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 29225a36364..ebcfc907ebb 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,13 +1,12 @@
- page_title "Members"
= render "header_title"
-- @blank_container = true
-.project-members-page
+.project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
.panel.panel-default
.panel-heading
Add new user to project
- .pull-right
+ .controls
= link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
Import members
.panel-body
@@ -19,3 +18,6 @@
- if @group
= render "group_members", members: @group_members
+
+ - if @project_group_links.any? && @project.allowed_to_share_with_group?
+ = render "shared_group_members"
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index db7f244d002..8ee2aef0e61 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -8,12 +8,9 @@
row.find("td.tree_time_ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
row.find("td.tree_commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
-- if @logs.present?
+- if @more_log_url
:plain
- var current_url = location.href.replace(/\/?$/, '/');
- var log_url = "#{escape_javascript(@log_url)}".replace(/\/?$/, '/');
-
- if(current_url == log_url) {
+ if($('#tree-slider').length) {
// Load more commit logs for each file in tree
// if we still on the same page
var url = "#{escape_javascript(@more_log_url)}";
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index bc80f2f29ad..c4a3f06ee06 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -9,9 +9,9 @@
%strong #{@tag.name}
.prepend-top-default
- = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form' }) do |f|
+ = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :description, classes: 'description js-quick-submit form-control'
+ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
= render 'projects/notes/hints'
.error-alert
.form-actions.prepend-top-default
diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml
index b9486a9b492..24658319060 100644
--- a/app/views/projects/repositories/_download_archive.html.haml
+++ b/app/views/projects/repositories/_download_archive.html.haml
@@ -10,7 +10,7 @@
%span.caret
%span.sr-only
Select Archive Format
- %ul.col-xs-10.dropdown-menu{ role: 'menu' }
+ %ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do
%i.fa.fa-download
diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml
index 315afe4a764..2d5b9f43c24 100644
--- a/app/views/projects/runners/index.html.haml
+++ b/app/views/projects/runners/index.html.haml
@@ -1,5 +1,6 @@
- page_title "Runners"
-.light
+
+.light.prepend-top-default
%p
A 'runner' is a process which runs a build.
You can setup as many runners as you need.
diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder
index 15c49767556..9b3d3f069d9 100644
--- a/app/views/projects/show.atom.builder
+++ b/app/views/projects/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_url(@project.namespace, @project)
- xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events[0].updated_at.xmlschema if @events[0]
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 7466a098e24..4310f038fc9 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity")
@@ -8,11 +10,10 @@
= render 'shared/no_password'
= render 'projects/last_push'
-
= render "home_panel"
.project-stats.gray-content-block.second-block
- %ul.nav.nav-pills
+ %ul.nav
%li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
= pluralize(number_with_delimiter(@project.commit_count), 'commit')
@@ -57,26 +58,17 @@
= link_to add_contribution_guide_path(@project) do
Add Contribution guide
-- if @project.archived?
- .text-warning.center.prepend-top-20
- %p
- = icon("exclamation-triangle fw")
- Archived project! Repository is read-only
-
- if @repository.commit
.content-block.second-block.white
- = render 'projects/last_commit', commit: @repository.commit, project: @project
+ %div{ class: container_class }
+ = render 'projects/last_commit', commit: @repository.commit, project: @project
-%div{class: "project-show-#{default_project_view}"}
- = render default_project_view
+%div{ class: container_class }
+ - if @project.archived?
+ .text-warning.center.prepend-top-20
+ %p
+ = icon("exclamation-triangle fw")
+ Archived project! Repository is read-only
-- if current_user
- - access = user_max_access_in_project(current_user.id, @project)
- - if access
- .prepend-top-20.project-footer
- .gray-content-block.footer-block.center
- You have #{access} access to this project.
- - if @project.project_member_by_id(current_user)
- = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
- data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project', class: 'cred' do
- Leave this project
+ %div{class: "project-show-#{default_project_view}"}
+ = render default_project_view
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 28b706c5c7e..399782273d3 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -3,7 +3,7 @@
%li
%div
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
- %strong
+ %span.item-title
= icon('tag')
= tag.name
- if tag.message.present?
diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml
new file mode 100644
index 00000000000..ffeacb5a004
--- /dev/null
+++ b/app/views/projects/tags/destroy.js.haml
@@ -0,0 +1,3 @@
+$('.js-totaltags-count').html("#{@repository.tags.size}");
+- if @repository.tags.empty?
+ $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 3a2f75fecaa..77c7c4d23de 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -10,7 +10,7 @@
New Tag
%hr
-= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-requires-input" do
+= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-quick-submit js-requires-input" do
.form-group
= label_tag :tag_name, nil, class: 'control-label'
.col-sm-10
@@ -30,7 +30,7 @@
= label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', attr: :release_description, classes: 'description js-quick-submit form-control'
+ = render 'projects/zen', attr: :release_description, classes: 'description form-control'
= render 'projects/notes/hints'
.help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
.form-actions
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index b594d4f1f27..8c7f93f93b6 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -18,7 +18,7 @@
= link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
%i.fa.fa-trash-o
.title
- %strong= @tag.name
+ %span.item-title= @tag.name
- if @tag.message.present?
%span.light
&nbsp;
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index 3c5edf4b033..baaa2caa6de 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,7 +1,7 @@
%article.file-holder.readme-holder
.file-title
= blob_icon readme.mode, readme.name
- = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, @path, readme.name)) do
%strong
= readme.name
.file-content.wiki
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 1927883513a..558e6146ae9 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,6 +1,6 @@
%div.tree-content-holder
.table-holder
- %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" }
+ %table.table#tree-slider{class: "table_#{@hex_path} tree-table" }
%thead
%tr
%th Name
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 3343288ad2b..3eb626e6dca 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -40,7 +40,7 @@
- continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
- - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
+ - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
= icon('pencil fw')
@@ -49,7 +49,7 @@
- continue_params = { to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to upload a file again.",
notice_now: edit_in_new_fork_notice_now }
- - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
+ - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
= icon('file fw')
@@ -58,7 +58,7 @@
- continue_params = { to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to create a new directory again.",
notice_now: edit_in_new_fork_notice_now }
- - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
+ - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
= icon('folder fw')
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index ec14bd7f65a..91fb2a44594 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -3,15 +3,15 @@
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
-
= render 'projects/last_push'
-- if can? current_user, :download_code, @project
- .tree-download-holder
- = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true
+.tree-controls
+ = render 'projects/find_file_link'
+ - if can? current_user, :download_code, @project
+ = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true
#tree-holder.tree-holder.clearfix
- .gray-content-block.top-block
+ .nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml
index e80dffc1ced..efe1e6f24c2 100644
--- a/app/views/projects/variables/show.html.haml
+++ b/app/views/projects/variables/show.html.haml
@@ -3,9 +3,11 @@
Secret Variables
%p.light
- These variables will be set to environment by the runner and will be hidden in the build log.
+ These variables will be set to environment by the runner.
%br
So you can use them for passwords, secret keys or whatever you want.
+ %br
+ The value of the variable can be visible in build log if explicitly asked to do so.
%hr
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 1d257818dcd..f0d1932e23c 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f|
-if @page.errors.any?
#error_explanation
.alert.alert-danger
@@ -15,7 +15,7 @@
= f.label :content, class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
- = render 'projects/zen', f: f, attr: :content, classes: 'description form-control js-quick-submit'
+ = render 'projects/zen', f: f, attr: :content, classes: 'description form-control'
= render 'projects/notes/hints'
.clearfix
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 29bf5d62abe..2b91b7e8f65 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,12 +1,11 @@
-%span.pull-right
- - if (@page && @page.persisted?)
- = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
- Page History
- - if can?(current_user, :create_wiki, @project)
- = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
- %i.fa.fa-pencil-square-o
- Edit
- - if can?(current_user, :admin_wiki, @project)
- = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
- = icon('trash')
- Delete
+- if (@page && @page.persisted?)
+ = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ Page History
+ - if can?(current_user, :create_wiki, @project)
+ = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ %i.fa.fa-pencil-square-o
+ Edit
+ - if can?(current_user, :admin_wiki, @project)
+ = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
+ = icon('trash')
+ Delete
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index e6e6ad5bc4b..a722fbc5352 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,13 +1,5 @@
-.project-issuable-filter
- .controls
- - if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
- %i.fa.fa-plus
- New Page
-
- = render 'projects/wikis/new'
-
- %ul.center-top-menu
+.top-area
+ %ul.nav-links
= nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
@@ -17,3 +9,11 @@
= nav_link(path: 'wikis#git_access') do
= link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
Git Access
+
+ .nav-controls
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ = icon('plus')
+ New Page
+
+= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index f0547e9c057..919daf0a7b2 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -5,12 +5,10 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title New Wiki Page
.modal-body
- = label_tag :new_wiki_path do
- %span Page slug
- = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project)
- %p.hidden.text-danger{data: { error: "slug" }}
- The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and /
- %p.hint
- Please don't use spaces.
- .form-actions
- = link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create'
+ %form.new-wiki-page
+ .form-group
+ = label_tag :new_wiki_path do
+ %span Page slug
+ = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true
+ .form-actions
+ = button_tag 'Create Page', class: 'build-new-wiki btn btn-create'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 23f64fbbd10..4dd818c7f67 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,16 +1,20 @@
- page_title "Edit", @page.title.capitalize, "Wiki"
= render "header_title"
-
= render 'nav'
-.gray-content-block
- .pull-right
+
+.top-area
+ .nav-text
+ %strong
+ - if @page.persisted?
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ - else
+ = @page.title.capitalize
+ %span.light
+ &middot;
+ Edit Page
+
+ .nav-controls
= render 'main_links'
- %h3.page-title.oneline
- %span.light Edit Page
- - if @page.persisted?
- = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
- - else
- = @page.title
= render 'form'
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 11c8c4f0eba..dd27ea2b11b 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -3,14 +3,12 @@
= render 'nav'
.gray-content-block
- .row
- .col-sm-6
- %h3.page-title.oneline
- Git access for
- %strong= @project_wiki.path_with_namespace
+ %span.oneline
+ Git access for
+ %strong= @project_wiki.path_with_namespace
- .col-sm-6
- = render "shared/clone_panel", project: @project_wiki
+ .pull-right
+ = render "shared/clone_panel", project: @project_wiki
.git-empty.prepend-top-default
%fieldset
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 4322146ce34..dcaddae2b04 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,11 +1,14 @@
- page_title "History", @page.title.capitalize, "Wiki"
= render "header_title"
-
= render 'nav'
-.gray-content-block
- %h3.page-title
- %span.light History for
- = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
+
+.top-area
+ .nav-text
+ %strong
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ %span.light
+ &middot;
+ History
.table-holder
%table.table
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index aae1ad69ad9..92b494a513c 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -2,15 +2,12 @@
= render "header_title"
= render 'nav'
-.gray-content-block
- All pages in this wiki are listed below.
-
+
%ul.content-list
- @wiki_pages.each do |wiki_page|
%li
- %h4
- = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
- %small (#{wiki_page.format})
- .pull-right
- %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
+ = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
+ %small (#{wiki_page.format})
+ .pull-right
+ %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
= paginate @wiki_pages, theme: 'gitlab'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 309d40f52bc..067fb7f8f54 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,17 +1,18 @@
- page_title @page.title.capitalize, "Wiki"
= render "header_title"
-
= render 'nav'
-.gray-content-block
- = render 'main_links'
- %h3.page-title.oneline
- = @page.title.capitalize
+.top-area
+ .nav-text
+ %strong= @page.title.capitalize
%span.wiki-last-edit-by
&middot;
last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+ .nav-controls
+ = render 'main_links'
+
- if @page.historical?
.warning_message
This is an old version of this page.
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 481451edb23..2c3fca439f3 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,4 +1,4 @@
-%ul.nav.nav-tabs.search-filter
+%ul.nav-links.search-filter
- if @project
%li{class: ("active" if @scope == 'blobs')}
= link_to search_filter_path(scope: 'blobs') do
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index ec478a5963d..4ef544136a8 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -6,14 +6,21 @@
- else
Any
%b.caret
- %ul.dropdown-menu
- %li
- = link_to search_filter_path(group_id: nil) do
- Any
- - current_user.authorized_groups.sort_by(&:name).each do |group|
- %li
- = link_to search_filter_path(group_id: group.id, project_id: nil) do
- = group.name
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Filter results by group
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-content
+ %ul
+ %li
+ = link_to search_filter_path(group_id: nil), class: ("is-active" if !params[:group_id].present?) do
+ Any
+ %li.divider
+ - current_user.authorized_groups.sort_by(&:name).each do |group|
+ %li
+ = link_to search_filter_path(group_id: group.id, project_id: nil), class: ("is-active" if params[:group_id] == group.id.to_s) do
+ = group.name
.dropdown.inline.prepend-left-10.project-filter
%button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'}
@@ -23,11 +30,18 @@
- else
Any
%b.caret
- %ul.dropdown-menu
- %li
- = link_to search_filter_path(project_id: nil) do
- Any
- - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project|
- %li
- = link_to search_filter_path(project_id: project.id, group_id: nil) do
- = project.name_with_namespace
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Filter results by project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-content
+ %ul
+ %li
+ = link_to search_filter_path(project_id: nil), class: ("is-active" if !params[:project_id].present?) do
+ Any
+ %li.divider
+ - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project|
+ %li
+ = link_to search_filter_path(project_id: project.id, group_id: nil), class: ("is-active" if params[:project_id] == project.id.to_s) do
+ = project.name_with_namespace
diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml
index 17b0981f073..a9dbc84da29 100644
--- a/app/views/search/_form.html.haml
+++ b/app/views/search/_form.html.haml
@@ -11,4 +11,4 @@
= button_tag 'Search', class: "btn btn-primary"
- unless params[:snippets].eql? 'true'
%br
- = render 'filter'
+ = render 'filter' if current_user
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 2a38c98dcfc..60df348891c 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,7 +1,7 @@
- if @search_results.empty?
= render partial: "search/results/empty"
- else
- %p.light
+ .gray-content-block
Search results for
%code
= @search_term
@@ -18,6 +18,8 @@
= render 'shared/projects/list', projects: @objects
- else
= render partial: "search/results/#{@scope.singularize}", collection: @objects
+
+ - if @scope != 'projects'
= paginate @objects, theme: 'gitlab'
:javascript
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 45d700781f3..710f5613c81 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -1,5 +1,6 @@
.search-result-row
%h4
+ = confidential_icon(issue)
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title
.pull-right ##{issue.iid}
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 2efa616d664..faeb2b55c6f 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -6,7 +6,7 @@
- if merge_request.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(merge_request.description))
+ = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project }))
%span.light
#{merge_request.project.name_with_namespace}
.pull-right
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 9a4f9fb9485..c9b7bd154af 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -1,50 +1,52 @@
+- snippet_blob = chunk_snippet(snippet_blob, @search_term)
+- snippet = snippet_blob[:snippet_object]
+- snippet_chunks = snippet_blob[:snippet_chunks]
+
.search-result-row
%span
- = snippet_blob[:snippet_object].title
+ = snippet.title
by
- = link_to user_snippets_path(snippet_blob[:snippet_object].author) do
- = image_tag avatar_icon(snippet_blob[:snippet_object].author_email), class: "avatar avatar-inline s16", alt: ''
- = snippet_blob[:snippet_object].author_name
- %span.light #{time_ago_with_tooltip(snippet_blob[:snippet_object].created_at)}
+ = link_to user_snippets_path(snippet.author) do
+ = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: ''
+ = snippet.author_name
+ %span.light #{time_ago_with_tooltip(snippet.created_at)}
%h4.snippet-title
- - snippet_path = reliable_snippet_path(snippet_blob[:snippet_object])
+ - snippet_path = reliable_snippet_path(snippet)
= link_to snippet_path do
.file-holder
.file-title
%i.fa.fa-file
- %strong= snippet_blob[:snippet_object].file_name
- - if markup?(snippet_blob[:snippet_object].file_name)
+ %strong= snippet.file_name
+ - if markup?(snippet.file_name)
.file-content.wiki
- - snippet_blob[:snippet_chunks].each do |snippet|
- - unless snippet[:data].empty?
- = render_markup(snippet_blob[:snippet_object].file_name, snippet[:data])
+ - snippet_chunks.each do |chunk|
+ - unless chunk[:data].empty?
+ = render_markup(snippet.file_name, chunk[:data])
- else
.file-content.code
.nothing-here-block Empty file
- else
- .file-content.code
- %div.highlighted-data{ class: user_color_scheme }
- .line-numbers
- - snippet_blob[:snippet_chunks].each do |snippet|
- - unless snippet[:data].empty?
- - snippet[:data].lines.to_a.size.times do |index|
- - offset = defined?(snippet[:start_line]) ? snippet[:start_line] : 1
- - i = index + offset
- = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}" do
- %i.fa.fa-link
- = i
- - unless snippet == snippet_blob[:snippet_chunks].last
+ .file-content.code.js-syntax-highlight
+ .line-numbers
+ - snippet_chunks.each do |chunk|
+ - unless chunk[:data].empty?
+ - Gitlab::Git::Util.count_lines(chunk[:data]).times do |index|
+ - offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1
+ - i = index + offset
+ = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do
+ %i.fa.fa-link
+ = i
+ - unless snippet == snippet_chunks.last
+ %a.diff-line-num
+ = "."
+ %pre.code
+ %code
+ - snippet_chunks.each do |chunk|
+ - unless chunk[:data].empty?
+ = chunk[:data]
+ - unless chunk == snippet_chunks.last
%a
- = "."
- .highlight.term
- %pre
- %code
- - snippet_blob[:snippet_chunks].each do |snippet|
- - unless snippet[:data].empty?
- = snippet[:data]
- - unless snippet == snippet_blob[:snippet_chunks].last
- %a
- = "..."
- - else
- .file-content.code
- .nothing-here-block Empty file
+ = "..."
+ - else
+ .file-content.code
+ .nothing-here-block Empty file
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index f5859481d46..235106c4f74 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -2,9 +2,9 @@
.blob-result
.file-holder
.file-title
- = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.filename) do
+ = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.basename) do
%i.fa.fa-file
%strong
- = wiki_blob.filename
+ = wiki_blob.basename
.file-content.code.term
= render 'shared/file_highlight', blob: wiki_blob, first_line_number: wiki_blob.startline
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index f4f3dcfc29f..215dbb3909e 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -1,5 +1,7 @@
- page_title @search_term
-= render 'search/form'
+
+.prepend-top-default
+ = render 'search/form'
- if @search_term
= render 'search/category'
= render 'search/results'
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 687a59c270f..faf7e49ed29 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -1,7 +1,7 @@
- project = project || @project
-.git-clone-holder
- .btn-group.clone-options
+.git-clone-holder.input-group
+ .input-group-btn
%a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'}
%span
= default_clone_protocol.upcase
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 7c57924277e..7afbaeddee8 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -7,7 +7,7 @@
.max-width-marker
= text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text]),
- class: 'form-control js-commit-message js-quick-submit', placeholder: local_assigns[:placeholder],
+ class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder],
required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}"
- if local_assigns[:hint]
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 8495774accc..c38d9313dba 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,4 +1,4 @@
-.btn-group.btn-group-next.event-filter
+%ul.nav-links.event-filter
= event_filter_link EventFilter.push, 'Push events'
= event_filter_link EventFilter.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments'
diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml
index 2bc98983d67..57856031d6e 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -1,13 +1,12 @@
-.file-content.code.js-syntax-highlight{ class: user_color_scheme }
+.file-content.code.js-syntax-highlight
.line-numbers
- if blob.data.present?
- - blob.data.lines.each_index do |index|
+ - blob.data.each_line.each_with_index do |_, index|
- offset = defined?(first_line_number) ? first_line_number : 1
- i = index + offset
-# We're not using `link_to` because it is too slow once we get to thousands of lines.
- %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
+ %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
%i.fa.fa-link
= i
.blob-content{data: {blob_id: blob.id}}
- :preserve
- #{highlight(blob.name, blob.data)}
+ = highlight(blob.name, blob.data)
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 285af56ad73..627814bcfae 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -11,6 +11,6 @@
%li
If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
%li
- The import will time out after 4 minutes. For big repositories, use a clone/push combination.
+ The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
%li
To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}.
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 4b4c9e9eabe..8ff9d4c1c7f 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -8,7 +8,7 @@
.pull-right
= link_to 'New issue', new_namespace_project_issue_path(project.namespace, project)
- %ul.well-list.issues-list
+ %ul.content-list.issues-list
- group[1].each do |issue|
= render 'projects/issues/issue', issue: issue
= paginate @issues, theme: "gitlab"
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
new file mode 100644
index 00000000000..8134b15d245
--- /dev/null
+++ b/app/views/shared/_label_row.html.haml
@@ -0,0 +1,4 @@
+%span.label-row
+ = link_to_label(label)
+ %span.prepend-left-10
+ = markdown(label.description, pipeline: :single_line)
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index da49c48acd3..b07f1c5603e 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,21 +1,9 @@
-<svg width="36px" height="36px" viewBox="0 0 210 210" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="tanuki-logo">
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
- <g id="logo" sketch:type="MSLayerGroup" transform="translate(0.000000, 10.000000)">
- <g id="Page-1" sketch:type="MSShapeGroup">
- <g id="Fill-1-+-Group-24">
- <g id="Group-24">
- <g id="Group">
- <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329" class="tanuki-shape"></path>
- <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26" class="tanuki-shape"></path>
- <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326" class="tanuki-shape"></path>
- <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329" class="tanuki-shape"></path>
- <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26" class="tanuki-shape"></path>
- <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326" class="tanuki-shape"></path>
- <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329" class="tanuki-shape"></path>
- </g>
- </g>
- </g>
- </g>
- </g>
- </g>
+<svg width="36" height="36" id="tanuki-logo">
+ <path id="tanuki-right-ear" class="tanuki-shape" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
+ <path id="tanuki-left-ear" class="tanuki-shape" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
+ <path id="tanuki-nose" class="tanuki-shape" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
+ <path id="tanuki-right-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/>
+ <path id="tanuki-left-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/>
+ <path id="tanuki-right-cheek" class="tanuki-shape" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/>
+ <path id="tanuki-left-cheek" class="tanuki-shape" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/>
</svg>
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index be17a511b26..e74fc36c797 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -8,7 +8,7 @@
.pull-right
= link_to 'New merge request', new_namespace_project_merge_request_path(project.namespace, project)
- %ul.well-list.mr-list
+ %ul.content-list.mr-list
- group[1].each do |merge_request|
= render 'projects/merge_requests/merge_request', merge_request: merge_request
= paginate @merge_requests, theme: "gitlab"
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index cbdecda4fff..cf16c203f9c 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,11 +1,10 @@
-.milestones-filters
- %ul.center-top-menu
- %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
- = link_to milestones_filter_path(state: 'opened') do
- Open
- %li{class: ("active" if params[:state] == 'closed')}
- = link_to milestones_filter_path(state: 'closed') do
- Closed
- %li{class: ("active" if params[:state] == 'all')}
- = link_to milestones_filter_path(state: 'all') do
- All
+%ul.nav-links
+ %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
+ = link_to milestones_filter_path(state: 'opened') do
+ Open
+ %li{class: ("active" if params[:state] == 'closed')}
+ = link_to milestones_filter_path(state: 'closed') do
+ Closed
+ %li{class: ("active" if params[:state] == 'all')}
+ = link_to milestones_filter_path(state: 'all') do
+ All
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index c4431d66927..1c58345278a 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,6 +1,6 @@
- if @projects.any?
- .prepend-left-10.new-project-item-select-holder
- = project_select_tag :project_path, class: "new-project-item-select", data: { include_groups: local_assigns[:include_groups] }
+ .prepend-left-10.project-item-select-holder
+ = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' }
%a.btn.btn-new.new-project-item-select-button
= icon('plus')
= local_assigns[:label]
@@ -8,12 +8,12 @@
:javascript
$('.new-project-item-select-button').on('click', function() {
- $('.new-project-item-select').select2('open');
+ $('.project-item-select').select2('open');
});
var relativePath = '#{local_assigns[:path]}';
- $('.new-project-item-select').on('click', function() {
+ $('.project-item-select').on('click', function() {
window.location = $(this).val() + '/' + relativePath;
});
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index 089179e677a..bb5fff2d3bb 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -1,6 +1,6 @@
- if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key?
.no-ssh-key-message.alert.alert-warning.hidden-xs
- You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path, class: 'alert-link'} to your profile
+ You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', profile_keys_path, class: 'alert-link'} to your profile
.pull-right
= link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link'
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
index 960ff00b49d..f4eb8e491b9 100644
--- a/app/views/shared/_project_limit.html.haml
+++ b/app/views/shared/_project_limit.html.haml
@@ -1,4 +1,4 @@
-- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project?
+- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? && current_user.projects_limit > 0
.project-limit-message.alert.alert-warning.hidden-xs
You won't be able to create new projects because you have reached your project limit.
diff --git a/app/views/shared/_promo.html.haml b/app/views/shared/_promo.html.haml
index 3596aabe309..09edf4000d5 100644
--- a/app/views/shared/_promo.html.haml
+++ b/app/views/shared/_promo.html.haml
@@ -1,5 +1,5 @@
.gitlab-promo
= link_to 'Homepage', promo_url
- = link_to "Blog", promo_url + '/blog/'
- = link_to "@gitlab", "https://twitter.com/gitlab"
- = link_to "Requests", "http://feedback.gitlab.com/"
+ = link_to 'Blog', promo_url + '/blog/'
+ = link_to '@gitlab', 'https://twitter.com/gitlab'
+ = link_to 'Requests', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#feature-proposals'
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 28d6f421fea..5a60ff5a5da 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -50,7 +50,7 @@
= form.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
- This url will be triggered when an issue is created
+ This url will be triggered when an issue is created/updated/merged
- if @service.supported_events.include?("merge_request")
%div
= form.check_box :merge_requests_events, class: 'pull-left'
@@ -58,7 +58,7 @@
= form.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
- This url will be triggered when a merge request is created
+ This url will be triggered when a merge request is created/updated/merged
- if @service.supported_events.include?("build")
%div
= form.check_box :build_events, class: 'pull-left'
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index af3d35de325..e3a6a5a68b6 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,6 +1,6 @@
.dropdown.inline.prepend-left-10
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light sort:
+ %span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
@@ -20,3 +20,7 @@
= sort_title_milestone_soon
= link_to page_filter_path(sort: sort_value_milestone_later) do
= sort_title_milestone_later
+ = link_to page_filter_path(sort: sort_value_upvotes) do
+ = sort_title_upvotes
+ = link_to page_filter_path(sort: sort_value_downvotes) do
+ = sort_title_downvotes
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index a54c5fa8c33..fb9a8db0889 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -1,5 +1,8 @@
- group_member = local_assigns[:group_member]
-%li
+- css_class = '' unless local_assigns[:css_class]
+- css_class += " no-description" if group.description.blank?
+
+%li.group-row{ class: css_class }
- if group_member
.controls.hidden-xs
- if can?(current_user, :admin_group, group)
@@ -9,14 +12,23 @@
= link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do
%i.fa.fa-sign-out
- = image_tag group_icon(group), class: "avatar s46 hidden-xs"
- = link_to group, class: 'group-name' do
- %strong= group.name
+ .stats
+ %span
+ = icon('home')
+ = number_with_delimiter(group.projects.count)
+
+ %span
+ = icon('users')
+ = number_with_delimiter(group.users.count)
+
+ = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ = link_to group, class: 'group-name title' do
+ = group.name
- if group_member
as
%span #{group_member.human_access}
- %div.light
- #{pluralize(group.projects.count, "project")}, #{pluralize(group.users.count, "user")}
-
+ - if group.description.present?
+ .description
+ = markdown(group.description, pipeline: :description)
diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml
new file mode 100644
index 00000000000..1aa7ed1f2eb
--- /dev/null
+++ b/app/views/shared/groups/_list.html.haml
@@ -0,0 +1,6 @@
+- if groups.any?
+ %ul.content-list
+ - groups.each_with_index do |group, i|
+ = render "shared/groups/group", group: group
+- else
+ %h3 No groups found
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index be06738eac9..dfdc84ba4cc 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,28 +1,5 @@
.issues-filters
- .issues-state-filters
- %ul.center-top-menu
- %li{class: ("active" if params[:state] == 'opened')}
- = link_to page_filter_path(state: 'opened') do
- #{state_filters_text_for(:opened, @project)}
-
- - if defined?(type) && type == :merge_requests
- %li{class: ("active" if params[:state] == 'merged')}
- = link_to page_filter_path(state: 'merged') do
- #{state_filters_text_for(:merged, @project)}
-
- %li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed') do
- #{state_filters_text_for(:closed, @project)}
- - else
- %li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed') do
- #{state_filters_text_for(:closed, @project)}
-
- %li{class: ("active" if params[:state] == 'all')}
- = link_to page_filter_path(state: 'all') do
- #{state_filters_text_for(:all, @project)}
-
- .issues-details-filters.gray-content-block
+ .issues-details-filters.gray-content-block.second-block
= 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' && can?(current_user, :admin_issue, @project)
.check-all-holder
@@ -30,22 +7,77 @@
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
- = users_select_tag(:author_id, selected: params[:author_id],
- placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
+ - if params[:author_id]
+ = hidden_field_tag(:author_id, params[:author_id])
+ = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author",
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id" } })
.filter-item.inline
- = users_select_tag(:assignee_id, selected: params[:assignee_id],
- placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true)
+ - if params[:assignee_id]
+ = hidden_field_tag(:assignee_id, params[:assignee_id])
+ = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee",
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } })
.filter-item.inline.milestone-filter
- = select_tag('milestone_title', projects_milestones_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Milestone'})
+ - if params[:milestone_title]
+ = hidden_field_tag(:milestone_title, params[:milestone_title])
+ = dropdown_tag(h(params[:milestone_name] || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
+ placeholder: "Search milestones", footer_content: true, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: (@project.id if @project), milestones: (namespace_project_milestones_path(@project.namespace, @project, :js) if @project) } }) do
+ - if @project
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_milestone, @project
+ %li
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
+ Create new
+ %li
+ = link_to namespace_project_milestones_path(@project.namespace, @project) do
+ - if can? current_user, :admin_milestone, @project
+ Manage milestones
+ - else
+ View milestones
.filter-item.inline.labels-filter
- = select_tag('label_name', projects_labels_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Label'})
+ - if params[:label_name]
+ = hidden_field_tag(:label_name, params[:label_name])
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: (@project.id if @project), labels: (namespace_project_labels_path(@project.namespace, @project, :js) if @project)}}
+ %span.dropdown-toggle-text
+ = h(params[:label_name] || "Label")
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-page-one
+ = dropdown_title("Filter by label")
+ = dropdown_filter("Search labels")
+ = dropdown_content
+ - if @project
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_label, @project
+ %li
+ %a.dropdown-toggle-page{href: "#"}
+ Create new
+ %li
+ = link_to namespace_project_labels_path(@project.namespace, @project) do
+ - if can? current_user, :admin_label, @project
+ Manage labels
+ - else
+ View labels
+ - if can? current_user, :admin_label, @project
+ .dropdown-page-two
+ = dropdown_title("Create new label", back: true)
+ = dropdown_content do
+ %input#new_label_color{type: "hidden"}
+ %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
+ .dropdown-label-color-preview.js-dropdown-label-color-preview
+ .suggest-colors.suggest-colors-dropdown
+ - suggested_colors.each do |color|
+ = link_to '#', style: "background-color: #{color}", data: { color: color } do
+ &nbsp
+ %button.btn.btn-primary.js-new-label-btn{type: "button"}
+ Create
+ = dropdown_loading
+ .dropdown-loading
+ = icon('spinner spin')
.pull-right
= render 'shared/sort_dropdown'
@@ -54,18 +86,32 @@
.issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
.filter-item.inline
- = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), include_blank: true, data: { placeholder: "Status" })
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ %ul
+ %li
+ %a{href: "#", data: {id: "reopen"}} Open
+ %li
+ %a{href: "#", data: {id: "close"}} Closed
.filter-item.inline
- = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true)
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline
- = select_tag('update[milestone_id]', bulk_update_milestone_options, include_blank: true, data: { placeholder: "Milestone" })
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable",
+ placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } })
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
= button_tag "Update issues", class: "btn update_selected_issues btn-save"
+- if @label
+ .gray-content-block.second-block
+ = render "shared/label_row", label: @label
+
:javascript
new UsersSelect();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new IssueStatusSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 90dc0062481..9ef729e960c 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -9,7 +9,7 @@
= f.label :title, class: 'control-label'
.col-sm-10
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
- class: 'form-control pad js-gfm-input js-quick-submit', required: true
+ class: 'form-control pad js-gfm-input', required: true
- if issuable.is_a?(MergeRequest)
%p.help-block
@@ -25,10 +25,19 @@
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description,
- classes: 'description form-control js-quick-submit'
+ classes: 'description form-control'
= render 'projects/notes/hints'
.clearfix
.error-alert
+
+- if issuable.is_a?(Issue) && !issuable.project.private?
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :confidential do
+ = f.check_box :confidential
+ This issue is confidential and should only be visible to team members
+
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
%hr
.form-group
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
new file mode 100644
index 00000000000..a6970b7eebb
--- /dev/null
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -0,0 +1,25 @@
+%ul.nav-links.issues-state-filters
+ - if defined?(type) && type == :merge_requests
+ - page_context_word = 'merge requests'
+ - else
+ - page_context_word = 'issues'
+ %li{class: ("active" if params[:state] == 'opened')}
+ = link_to page_filter_path(state: 'opened'), title: "Filter by #{page_context_word} that are currently opened." do
+ #{state_filters_text_for(:opened, @project)}
+
+ - if defined?(type) && type == :merge_requests
+ %li{class: ("active" if params[:state] == 'merged')}
+ = link_to page_filter_path(state: 'merged'), title: 'Filter by merge requests that are currently merged.' do
+ #{state_filters_text_for(:merged, @project)}
+
+ %li{class: ("active" if params[:state] == 'closed')}
+ = link_to page_filter_path(state: 'closed'), title: 'Filter by merge requests that are currently closed and unmerged.' do
+ #{state_filters_text_for(:closed, @project)}
+ - else
+ %li{class: ("active" if params[:state] == 'closed')}
+ = link_to page_filter_path(state: 'closed'), title: 'Filter by issues that are currently closed.' do
+ #{state_filters_text_for(:closed, @project)}
+
+ %li{class: ("active" if params[:state] == 'all')}
+ = link_to page_filter_path(state: 'all'), title: "Show all #{page_context_word}." do
+ #{state_filters_text_for(:all, @project)}
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index da6bacbb74a..f1d92ef48b2 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -1,5 +1,10 @@
.block.participants
- .title
+ .sidebar-collapsed-icon
+ = icon('users')
+ %span
+ = participants.count
+ .title.hide-collapsed
= pluralize participants.count, "participant"
- participants.each do |participant|
- = link_to_member(@project, participant, name: false, size: 24)
+ %span.hide-collapsed
+ = link_to_member(@project, participant, name: false, size: 24)
diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml
index 3a5ad00aa91..afad48499b7 100644
--- a/app/views/shared/issuable/_search_form.html.haml
+++ b/app/views/shared/issuable/_search_form.html.haml
@@ -1,9 +1,8 @@
-= 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', spellcheck: false }
- = hidden_field_tag :state, params['state']
- = hidden_field_tag :scope, params['scope']
- = hidden_field_tag :assignee_id, params['assignee_id']
- = hidden_field_tag :author_id, params['author_id']
- = hidden_field_tag :milestone_id, params['milestone_id']
- = hidden_field_tag :label_id, params['label_id']
+= form_tag(path, method: :get, id: "issue_search_form", class: 'issue-search-form') do
+ = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by name ...', class: 'form-control issue_search search-text-input input-short', spellcheck: false }
+ = hidden_field_tag :state, params['state']
+ = hidden_field_tag :scope, params['scope']
+ = hidden_field_tag :assignee_id, params['assignee_id']
+ = hidden_field_tag :author_id, params['author_id']
+ = hidden_field_tag :milestone_id, params['milestone_id']
+ = hidden_field_tag :label_id, params['label_id']
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 79c5cc7f40a..23b1ed1e51b 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,83 +1,128 @@
-.issuable-sidebar.issuable-affix
- = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
- .block.assignee
- .title
- %label
- Assignee
- - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value
- - if issuable.assignee
- %strong= link_to_member(@project, issuable.assignee, size: 24)
+%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
+ .issuable-sidebar
+ .block
+ %span.issuable-count.hide-collapsed.pull-left
+ = issuable.iid
+ of
+ = issuables_count(issuable)
+ %span.pull-right
+ %a.gutter-toggle.js-sidebar-toggle{href: '#'}
+ = sidebar_gutter_toggle_icon
+ .issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
+ - if prev_issuable = prev_issuable_for(issuable)
+ = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn'
+ - else
+ %a.btn.btn-default.disabled{href: '#'}
+ Prev
+ - if next_issuable = next_issuable_for(issuable)
+ = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn'
- else
- .light None
+ %a.btn.btn-default.disabled{href: '#'}
+ Next
- .selectbox
- = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true)
+ = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
+ .block.assignee
+ .sidebar-collapsed-icon
+ - if issuable.assignee
+ = link_to_member_avatar(issuable.assignee, size: 24)
+ - else
+ = icon('user')
+ .title.hide-collapsed
+ %label
+ Assignee
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ .pull-right
+ = link_to 'Edit', '#', class: 'edit-link'
+ .value.hide-collapsed
+ - if issuable.assignee
+ %strong= link_to_member(@project, issuable.assignee, size: 24)
+ - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
+ %a.pull-right.cannot-be-merged{href: '#', data: {toggle: 'tooltip'}, title: 'Not allowed to merge'}
+ = icon('exclamation-triangle')
+ - else
+ .light None
- .block.milestone
- .title
- %label
- Milestone
- - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value
- - if issuable.milestone
- %span.back-to-milestone
- = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
- %strong
- = icon('clock-o')
- = issuable.milestone.title
- - else
- .light None
- .selectbox
- = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }})
- = hidden_field_tag :issuable_context
- = f.submit class: 'btn hide'
+ .selectbox.hide-collapsed
+ = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true)
- - if issuable.project.labels.any?
- .block
- .title
- %label Labels
+ .block.milestone
+ .sidebar-collapsed-icon
+ = icon('clock-o')
+ %span
+ - if issuable.milestone
+ = issuable.milestone.title
+ - else
+ No
+ .title.hide-collapsed
+ %label
+ Milestone
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.pull-right
= link_to 'Edit', '#', class: 'edit-link'
- .value.issuable-show-labels
- - if issuable.labels.any?
- - issuable.labels.each do |label|
- = link_to_label(label)
+ .value.hide-collapsed
+ - if issuable.milestone
+ %span.back-to-milestone
+ = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
+ %strong
+ = icon('clock-o')
+ = issuable.milestone.title
- else
.light None
- .selectbox
- = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
- { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" }
+ .selectbox.hide-collapsed
+ = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }})
+ = hidden_field_tag :issuable_context
+ = f.submit class: 'btn hide'
- .block
- .title
- Cross-project reference
- .cross-project-reference
- %span#cross-project-reference
- = cross_project_reference(@project, issuable)
- = clipboard_button(clipboard_target: 'span#cross-project-reference')
+ - if issuable.project.labels.any?
+ .block.labels
+ .sidebar-collapsed-icon
+ = icon('tags')
+ %span
+ = issuable.labels.count
+ .title.hide-collapsed
+ %label Labels
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ .pull-right
+ = link_to 'Edit', '#', class: 'edit-link'
+ .value.issuable-show-labels.hide-collapsed
+ - if issuable.labels.any?
+ - issuable.labels.each do |label|
+ = link_to_label(label, type: issuable.to_ability_name)
+ - else
+ .light None
+ .selectbox.hide-collapsed
+ = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
+ { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" }
- = render "shared/issuable/participants", participants: issuable.participants(current_user)
+ = render "shared/issuable/participants", participants: issuable.participants(current_user)
+ %hr
+ - if current_user
+ - subscribed = issuable.subscribed?(current_user)
+ .block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
+ .sidebar-collapsed-icon
+ = icon('rss')
+ .title.hide-collapsed
+ %label.light Notifications
+ - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
+ %button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'}
+ %span= subscribed ? 'Unsubscribe' : 'Subscribe'
+ .subscription-status.hide-collapsed{data: {status: subscribtion_status}}
+ .unsubscribed{class: ( 'hidden' if subscribed )}
+ You're not receiving notifications from this thread.
+ .subscribed{class: ( 'hidden' unless subscribed )}
+ You're receiving notifications because you're subscribed to this thread.
- - if current_user
- - subscribed = issuable.subscribed?(current_user)
- .block.light
- .title
- %label.light Notifications
- - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-block.btn-gray.subscribe-button{:type => 'button'}
- %span= subscribed ? 'Unsubscribe' : 'Subscribe'
- .subscription-status{data: {status: subscribtion_status}}
- .unsubscribed{class: ( 'hidden' if subscribed )}
- You're not receiving notifications from this thread.
- .subscribed{class: ( 'hidden' unless subscribed )}
- You're receiving notifications because you're subscribed to this thread.
+ - project_ref = cross_project_reference(@project, issuable)
+ .block.project-reference
+ .sidebar-collapsed-icon
+ = clipboard_button(clipboard_text: project_ref)
+ .cross-project-reference.hide-collapsed
+ %span
+ Reference:
+ %cite{title: project_ref}
+ = project_ref
+ = clipboard_button(clipboard_text: project_ref)
- :javascript
- new Subscription("#{toggle_subscription_path(issuable)}");
- new IssuableContext();
+ :javascript
+ new Subscription('.subscription');
+ new IssuableContext();
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
new file mode 100644
index 00000000000..85888096722
--- /dev/null
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -0,0 +1,27 @@
+-# @project is present when viewing Project's milestone
+- project = @project || issuable.project
+- assignee = issuable.assignee
+- issuable_type = issuable.class.table_name
+- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
+
+%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) }
+ %span
+ - if show_project_name
+ %strong #{project.name} &middot;
+ - elsif show_full_project_name
+ %strong #{project.name_with_namespace} &middot;
+ - if issuable.is_a?(Issue)
+ = confidential_icon(issuable)
+ = link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title
+ %div{class: 'issuable-detail'}
+ = link_to [project.namespace.becomes(Namespace), project, issuable] do
+ %span{ class: 'issuable-number' }>= issuable.to_reference
+
+ - issuable.labels.each do |label|
+ = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
+ - render_colored_label(label)
+
+ - if assignee
+ = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
+ class: 'has_tooltip', data: { 'original-title' => "Assigned to #{sanitize(assignee.name)}", container: 'body' } do
+ - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
new file mode 100644
index 00000000000..8619939dde7
--- /dev/null
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -0,0 +1,16 @@
+- show_counter = local_assigns.fetch(:show_counter, false)
+- primary = local_assigns.fetch(:primary, false)
+- panel_class = primary ? 'panel-primary' : 'panel-default'
+
+.panel{ class: panel_class }
+ .panel-heading
+ = title
+ - if show_counter
+ .pull-right= issuables.size
+
+ - class_prefix = dom_class(issuables).pluralize
+ %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
+ = render partial: 'shared/milestones/issuable',
+ collection: issuables.sort_by(&:position),
+ as: :issuable,
+ locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name }
diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml
new file mode 100644
index 00000000000..a8db7f8a556
--- /dev/null
+++ b/app/views/shared/milestones/_issues_tab.html.haml
@@ -0,0 +1,10 @@
+- args = { show_project_name: local_assigns.fetch(:show_project_name, false),
+ show_full_project_name: local_assigns.fetch(:show_full_project_name, false) }
+
+.row.prepend-top-default
+ .col-md-4
+ = render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true)
+ .col-md-4
+ = render 'shared/milestones/issuables', args.merge(title: 'Ongoing Issues (open and assigned)', issuables: issues.opened.assigned, id: 'ongoing', show_counter: true)
+ .col-md-4
+ = render 'shared/milestones/issuables', args.merge(title: 'Completed Issues (closed)', issuables: issues.closed, id: 'closed', show_counter: true)
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
new file mode 100644
index 00000000000..ba27bafd1bc
--- /dev/null
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -0,0 +1,18 @@
+%ul.bordered-list.manage-labels-list
+ - labels.each do |label|
+ - options = { milestone_title: @milestone.title, label_name: label.title }
+
+ %li
+ %span.label-row
+ = link_to milestones_label_path(options) do
+ - render_colored_label(label)
+ %span.prepend-left-10
+ = markdown(label.description, pipeline: :single_line)
+
+ .pull-right
+ %strong.issues-count
+ = link_to milestones_label_path(options.merge(state: 'opened')) do
+ - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue'
+ %strong.issues-count
+ = link_to milestones_label_path(options.merge(state: 'closed')) do
+ - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue'
diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml
new file mode 100644
index 00000000000..c29d8ee6737
--- /dev/null
+++ b/app/views/shared/milestones/_merge_requests_tab.haml
@@ -0,0 +1,12 @@
+- args = { show_project_name: local_assigns.fetch(:show_project_name, false),
+ show_full_project_name: local_assigns.fetch(:show_full_project_name, false) }
+
+.row.prepend-top-default
+ .col-md-3
+ = render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned')
+ .col-md-3
+ = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing')
+ .col-md-3
+ = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed')
+ .col-md-3
+ = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true)
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
new file mode 100644
index 00000000000..6b25745c554
--- /dev/null
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -0,0 +1,45 @@
+- dashboard = local_assigns[:dashboard]
+- custom_dom_id = dom_id(@project ? milestone : milestone.milestones.first)
+
+%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
+ .row
+ .col-sm-6
+ %strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path
+ .col-sm-6
+ .pull-right.light #{milestone.percent_complete(current_user)}% complete
+ .row
+ .col-sm-6
+ = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path
+ &middot;
+ = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
+ .col-sm-6= milestone_progress_bar(milestone)
+ - if milestone.is_a?(GlobalMilestone)
+ .row
+ .col-sm-6
+ .expiration= render('shared/milestone_expired', milestone: milestone)
+ .projects
+ - milestone.milestones.each do |milestone|
+ = link_to milestone_path(milestone) do
+ %span.label.label-gray
+ = dashboard ? milestone.project.name_with_namespace : milestone.project.name
+ - if @group
+ .col-sm-6
+ - if can?(current_user, :admin_milestones, @group)
+ - if milestone.closed?
+ = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
+ - else
+ = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close"
+
+ - if @project
+ .row
+ .col-sm-6= render('shared/milestone_expired', milestone: milestone)
+ .col-sm-6
+ - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
+ = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do
+ = icon('pencil-square-o')
+ Edit
+ \
+ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close"
+ = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
+ = icon('trash-o')
+ Delete
diff --git a/app/views/shared/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml
new file mode 100644
index 00000000000..67ae85ac276
--- /dev/null
+++ b/app/views/shared/milestones/_participants_tab.html.haml
@@ -0,0 +1,8 @@
+%ul.bordered-list
+ - users.each do |user|
+ %li
+ = link_to user, title: user.name, class: "darken" do
+ = image_tag avatar_icon(user, 32), class: "avatar s32"
+ %strong= truncate(user.name, lenght: 40)
+ %br
+ %small.cgray= user.username
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
new file mode 100644
index 00000000000..385c6596606
--- /dev/null
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -0,0 +1,28 @@
+- project = local_assigns[:project]
+
+.context.prepend-top-default
+ .milestone-summary
+ %h4 Progress
+ %strong= milestone.issues_visible_to_user(current_user).size
+ issues:
+ %span.milestone-stat
+ %strong= milestone.issues_visible_to_user(current_user).opened.size
+ open and
+ %strong= milestone.issues_visible_to_user(current_user).closed.size
+ closed
+ %span.milestone-stat
+ %strong== #{milestone.percent_complete(current_user)}%
+ complete
+
+ %span.milestone-stat
+ %span.remaining-days= milestone_remaining_days(milestone)
+ %span.pull-right.tab-issues-buttons
+ - if project && can?(current_user, :create_issue, project)
+ = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do
+ %i.fa.fa-plus
+ New Issue
+ = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped"
+ %span.pull-right.tab-merge-requests-buttons.hidden
+ = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped"
+
+ = milestone_progress_bar(milestone)
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
new file mode 100644
index 00000000000..2b6ce2d7e7a
--- /dev/null
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -0,0 +1,30 @@
+%ul.nav-links.no-top.no-bottom
+ %li.active
+ = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
+ Issues
+ %span.badge= milestone.issues_visible_to_user(current_user).size
+ %li
+ = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
+ Merge Requests
+ %span.badge= milestone.merge_requests.size
+ %li
+ = link_to '#tab-participants', 'data-toggle' => 'tab' do
+ Participants
+ %span.badge= milestone.participants.count
+ %li
+ = link_to '#tab-labels', 'data-toggle' => 'tab' do
+ Labels
+ %span.badge= milestone.labels.count
+
+- show_project_name = local_assigns.fetch(:show_project_name, false)
+- show_full_project_name = local_assigns.fetch(:show_full_project_name, false)
+
+.tab-content.milestone-content
+ .tab-pane.active#tab-issues
+ = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ .tab-pane#tab-merge-requests
+ = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ .tab-pane#tab-participants
+ = render 'shared/milestones/participants_tab', users: milestone.participants
+ .tab-pane#tab-labels
+ = render 'shared/milestones/labels_tab', labels: milestone.labels
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
new file mode 100644
index 00000000000..cab8743a077
--- /dev/null
+++ b/app/views/shared/milestones/_top.html.haml
@@ -0,0 +1,58 @@
+- page_title milestone.title, "Milestones"
+
+- group = local_assigns[:group]
+
+.detail-page-header
+ .status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" }
+ - if milestone.closed?
+ Closed
+ - elsif milestone.expired?
+ Expired
+ - else
+ Open
+ %span.identifier
+ Milestone #{milestone.title}
+ - if milestone.expires_at
+ %span.creator
+ &middot;
+ = milestone.expires_at
+ - if group
+ .pull-right
+ - if can?(current_user, :admin_milestones, group)
+ - if milestone.active?
+ = link_to 'Close Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close"
+ - else
+ = link_to 'Reopen Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
+
+.detail-page-description.gray-content-block.second-block
+ %h2.title
+ = markdown escape_once(milestone.title), pipeline: :single_line
+
+- if milestone.complete?(current_user) && milestone.active?
+ .alert.alert-success.prepend-top-default
+ - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
+ %span All issues for this milestone are closed. #{close_msg}
+
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %th Project
+ %th Open issues
+ %th State
+ %th Due date
+ - milestone.milestones.each do |ms|
+ %tr
+ %td
+ - project_name = group ? ms.project.name : ms.project.name_with_namespace
+ = link_to project_name, namespace_project_milestone_path(ms.project.namespace, ms.project, ms)
+ %td
+ = ms.issues_visible_to_user(current_user).opened.count
+ %td
+ - if ms.closed?
+ Closed
+ - else
+ Open
+ %td
+ = ms.expires_at
+
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
new file mode 100644
index 00000000000..e7e04621ff4
--- /dev/null
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -0,0 +1,22 @@
+- @sort ||= sort_value_recently_updated
+- archived = params[:archived]
+.dropdown.inline
+ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light
+ = projects_sort_options_hash[@sort]
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ Sort by
+ - projects_sort_options_hash.each do |value, title|
+ %li
+ = link_to filter_projects_path(sort: value, archived: archived), class: ("is-active" if @sort == value) do
+ = title
+
+ %li.divider
+ %li
+ = link_to filter_projects_path(sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do
+ Hide archived projects
+ %li
+ = link_to filter_projects_path(sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do
+ Show archived projects
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index e5ffe1e29ae..2e08bb2ac08 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -1,21 +1,24 @@
- projects_limit = 20 unless local_assigns[:projects_limit]
- avatar = true unless local_assigns[:avatar] == false
+- use_creator_avatar = false unless local_assigns[:use_creator_avatar] == true
- stars = true unless local_assigns[:stars] == false
+- forks = false unless local_assigns[:forks] == true
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
+- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
+- remote = false unless local_assigns[:remote] == true
-%ul.projects-list
- - projects.each_with_index do |project, i|
- - css_class = (i >= projects_limit) ? 'hide' : nil
- = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
- avatar: avatar, stars: stars, css_class: css_class, ci: ci
-
- - if projects.size > projects_limit
- %li.bottom.center
- .light
- #{projects_limit} of #{pluralize(projects.count, 'project')} displayed.
- = link_to '#', class: 'js-expand' do
- Show all
+.projects-list-holder
+ - if projects.any?
+ %ul.projects-list.content-list
+ - projects.each_with_index do |project, i|
+ - css_class = (i >= projects_limit) ? 'hide' : nil
+ = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
+ avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
+ forks: forks, show_last_commit_as_description: show_last_commit_as_description
+ = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
+ - else
+ .nothing-here-block No projects found
:javascript
- new ProjectsList();
+ ProjectsList.init();
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 86249851a82..97cfb76cdb0 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -1,16 +1,25 @@
- avatar = true unless local_assigns[:avatar] == false
- stars = true unless local_assigns[:stars] == false
+- forks = false unless local_assigns[:forks] == true
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- css_class = '' unless local_assigns[:css_class]
-- css_class += " no-description" unless project.description.present?
+- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
+- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
+- ci_commit = project.ci_commit(project.commit.sha) if ci && !project.empty_repo? && project.commit
+- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2']
+- cache_key.push(ci_commit.status) if ci_commit
+
%li.project-row{ class: css_class }
- = cache [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2'] do
+ = cache(cache_key) do
= link_to project_path(project), class: dom_class(project) do
- if avatar
.dash-project-avatar
- = project_icon(project, alt: '', class: 'avatar project-avatar s46')
- %span.project-full-name
+ - if use_creator_avatar
+ = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
+ - else
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ %span.project-full-name.title
%span.namespace-name
- if project.namespace && !skip_namespace
= project.namespace.human_name
@@ -18,15 +27,28 @@
%span.project-name.filter-title
= project.name
- .project-controls
- - if ci && !project.empty_repo? && project.commit
- - if ci_commit = project.ci_commit(project.commit.sha)
+ .controls
+ - if project.main_language
+ %span
+ = project.main_language
+ - if ci_commit
+ %span
= render_ci_status(ci_commit)
- &nbsp;
+ - if forks
+ %span
+ = icon('code-fork')
+ = project.forks_count
- if stars
%span
- %i.fa.fa-star
+ = icon('star')
= project.star_count
- - if project.description.present?
- .project-description
+ %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' },
+ title: "#{visibility_level_label(project.visibility_level)} - #{project_visibility_level_description(project.visibility_level)}"}
+ = visibility_level_icon(project.visibility_level, fw: false)
+ - if show_last_commit_as_description
+ .description
+ = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit),
+ class: "commit-row-message"
+ - elsif project.description.present?
+ .description
= markdown(project.description, pipeline: :description)
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index d26a99bb14c..773ce8ac240 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,10 +1,11 @@
- unless @snippet.content.empty?
- if markup?(@snippet.file_name)
+ %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
+ = @snippet.data
.file-content.wiki
= render_markup(@snippet.file_name, @snippet.data)
- else
- .file-content.code
- = render 'shared/file_highlight', blob: @snippet
+ = render 'shared/file_highlight', blob: @snippet
- else
.file-content.code
.nothing-here-block Empty file
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index c6294caddc7..a316a085107 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,10 +1,12 @@
%li.snippet-row
+ = image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
+
.snippet-title
- = link_to reliable_snippet_path(snippet) do
+ = link_to reliable_snippet_path(snippet), class: 'title' do
= truncate(snippet.title, length: 60)
- if snippet.private?
%span.label.label-gray
- %i.fa.fa-lock
+ = icon('lock')
private
%span.monospace.pull-right
= snippet.file_name
@@ -15,6 +17,5 @@
.snippet-info
= link_to user_snippets_path(snippet.author) do
- = image_tag avatar_icon(snippet.author_email), class: "avatar s24", alt: ''
= snippet.author_name
authored #{time_ago_with_tooltip(snippet.created_at)}
diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml
index 4a84348ac82..83f61ce4b07 100644
--- a/app/views/sherlock/queries/show.html.haml
+++ b/app/views/sherlock/queries/show.html.haml
@@ -1,7 +1,7 @@
- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query')
- header_title t('sherlock.title'), sherlock_transactions_path
-%ul.center-top-menu
+%ul.nav-links
%li.active
%a(href="#tab-general" data-toggle="tab")
= t('sherlock.general')
diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml
index b7e0162e80d..b8d93e9ff45 100644
--- a/app/views/sherlock/transactions/_queries.html.haml
+++ b/app/views/sherlock/transactions/_queries.html.haml
@@ -8,7 +8,7 @@
%tr
%th= t('sherlock.time')
%th= t('sherlock.query')
- %td
+ %th
%tbody
- @transaction.sorted_queries.each do |query|
%tr
diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml
index 3c8ffb06648..9d4b0b2724c 100644
--- a/app/views/sherlock/transactions/show.html.haml
+++ b/app/views/sherlock/transactions/show.html.haml
@@ -1,7 +1,7 @@
- page_title t('sherlock.title'), t('sherlock.transaction')
- header_title t('sherlock.title'), sherlock_transactions_path
-%ul.center-top-menu
+%ul.nav-links
%li.active
%a(href="#tab-general" data-toggle="tab")
= t('sherlock.general')
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index d9aa4dd1d2e..80a3e731e1d 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,4 +1,4 @@
-%ul.bordered-list
+%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets
- if @snippets.empty?
%li
diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder
index 2fe5b7fac83..e9e466c6350 100644
--- a/app/views/users/show.atom.builder
+++ b/app/views/users/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml"
xml.link href: user_url(@user), rel: "alternate", type: "text/html"
xml.id user_url(@user)
- xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events[0].updated_at.xmlschema if @events[0]
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 0bca8177e14..bca816f22cb 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,120 +1,117 @@
- page_title @user.name
- page_description @user.bio
- header_title @user.name, user_path(@user)
+- @no_container = true
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
= render 'shared/show_aside'
-.cover-block
- .avatar-holder
- = link_to avatar_icon(@user, 400), target: '_blank' do
- = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
- .cover-title
- = @user.name
+.user-profile
+ .cover-block
+ .cover-controls
+ - if @user == current_user
+ = link_to profile_path, class: 'btn btn-gray' do
+ = icon('pencil')
+ - elsif current_user
+ %span.report-abuse
+ - if @user.abuse_report
+ %button.btn.btn-danger{ title: 'Already reported for abuse',
+ data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
+ = icon('exclamation-circle')
+ - else
+ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
+ title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
+ = icon('exclamation-circle')
+ - if current_user
+ &nbsp;
+ = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
+ = icon('rss')
+
+ .avatar-holder
+ = link_to avatar_icon(@user, 400), target: '_blank' do
+ = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
+ .cover-title
+ = @user.name
+
+ .cover-desc
+ %span.middle-dot-divider
+ @#{@user.username}
+ %span.middle-dot-divider
+ Member since #{@user.created_at.to_s(:medium)}
- .cover-desc
- %span
- @#{@user.username}.
- if @user.bio.present?
- %span
- #{@user.bio}.
- %span
- Member since #{@user.created_at.stamp("Aug 21, 2011")}
-
- .cover-desc
- - unless @user.public_email.blank?
- .profile-link-holder
- = link_to @user.public_email, "mailto:#{@user.public_email}"
- - unless @user.skype.blank?
- .profile-link-holder
- = link_to "skype:#{@user.skype}", title: "Skype" do
- = icon('skype')
- - unless @user.linkedin.blank?
- .profile-link-holder
- = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
- = icon('linkedin-square')
- - unless @user.twitter.blank?
- .profile-link-holder
- = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
- = icon('twitter-square')
- - unless @user.website_url.blank?
- .profile-link-holder
- = link_to @user.short_website_url, @user.full_website_url
- - unless @user.location.blank?
- .profile-link-holder
- = icon('map-marker')
- = @user.location
-
-
- .cover-controls
- - if @user == current_user
- = link_to profile_path, class: 'btn btn-gray' do
- = icon('pencil')
- - elsif current_user
- %span.report-abuse
- - if @user.abuse_report
- %button.btn.btn-danger{ title: 'Already reported for abuse',
- data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
- = icon('exclamation-circle')
- - else
- = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray',
- title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
- = icon('exclamation-circle')
- - if current_user
- &nbsp;
- = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
- = icon('rss')
-
-.gray-content-block.second-block
- .user-calendar
- %h4.center.light
- %i.fa.fa-spinner.fa-spin
- .user-calendar-activities
-
-
-%ul.center-top-menu.no-top.no-bottom.bottom-border.wide
- %li.active
- = link_to "#activity", 'data-toggle' => 'tab' do
- Activity
- - if @groups.any?
- %li
- = link_to "#groups", 'data-toggle' => 'tab' do
- Groups
- - if @contributed_projects.present?
- %li
- = link_to "#contributed", 'data-toggle' => 'tab' do
- Contributed projects
- - if @projects.present?
- %li
- = link_to "#personal", 'data-toggle' => 'tab' do
- Personal projects
-
-.tab-content
- .tab-pane.active#activity
- .content_list
- = spinner
-
- - if @groups.any?
- .tab-pane#groups
- %ul.content-list
- - @groups.each do |group|
- = render 'shared/groups/group', group: group
-
- - if @contributed_projects.present?
- .tab-pane#contributed
- .contributed-projects
- = render 'shared/projects/list',
- projects: @contributed_projects.sort_by(&:star_count).reverse,
- projects_limit: 5, stars: true, avatar: true
-
- - if @projects.present?
- .tab-pane#personal
- .personal-projects
- = render 'shared/projects/list',
- projects: @projects.sort_by(&:star_count).reverse,
- projects_limit: 10, stars: true, avatar: true
+ .cover-desc
+ %p.profile-user-bio
+ = @user.bio
+
+ .cover-desc
+ - unless @user.public_email.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.public_email, "mailto:#{@user.public_email}"
+ - unless @user.skype.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "skype:#{@user.skype}", title: "Skype" do
+ = icon('skype')
+ - unless @user.linkedin.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
+ = icon('linkedin-square')
+ - unless @user.twitter.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
+ = icon('twitter-square')
+ - unless @user.website_url.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.short_website_url, @user.full_website_url
+ - unless @user.location.blank?
+ .profile-link-holder.middle-dot-divider
+ = icon('map-marker')
+ = @user.location
+
+ %ul.nav-links.center.user-profile-nav
+ %li.activity-tab
+ = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
+ Activity
+ %li.groups-tab
+ = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
+ Groups
+ %li.contributed-tab
+ = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
+ Contributed projects
+ %li.projects-tab
+ = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
+ Personal projects
+
+ %div{ class: container_class }
+ .tab-content
+ #activity.tab-pane
+ .gray-content-block.white.second-block
+ %div{ class: container_class }
+ .user-calendar{data: {href: user_calendar_path}}
+ %h4.center.light
+ %i.fa.fa-spinner.fa-spin
+ .user-calendar-activities
+
+ .content_list{ data: {href: user_path} }
+ = spinner
+
+ #groups.tab-pane
+ - # This tab is always loaded via AJAX
+
+ #contributed.contributed-projects.tab-pane
+ - # This tab is always loaded via AJAX
+
+ #projects.tab-pane
+ - # This tab is always loaded via AJAX
+
+ .loading-status
+ = spinner
:javascript
- $(".user-calendar").load("#{user_calendar_path}");
+ var userProfile;
+
+ userProfile = new User({
+ action: "#{controller.action_name}"
+ });
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
index ce0a0113403..20d2d5f317b 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -1,46 +1,28 @@
.awards.votes-block
- awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
- .award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)}
+ %button.btn.award-control.js-emoji-btn.has_tooltip{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user), data: {placement: "top"}}
= emoji_icon(emoji)
- .counter
+ %span.award-control-text.js-counter
= notes.count
- if current_user
- .awards-controls
- %a.add-award{"data-toggle" => "dropdown", "data-target" => "#", "href" => "#"}
- = icon('smile-o')
- .emoji-menu
- .emoji-menu-content
- = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- - AwardEmoji.emoji_by_category.each do |category, emojis|
- %h5= AwardEmoji::CATEGORIES[category]
- %ul
- - emojis.each do |emoji|
- %li
- = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
+ %div.award-menu-holder.js-award-holder
+ %a.btn.award-control.js-add-award{"href" => "#"}
+ = icon('smile-o', {class: "award-control-icon"})
+ = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"})
+ %span.award-control-text
+ Add
- if current_user
- :coffeescript
- post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"
- noteable_type = "#{votable.class.name.underscore}"
- noteable_id = "#{votable.id}"
- aliases = #{AwardEmoji.aliases.to_json}
+ :javascript
+ var post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}";
+ var noteable_type = "#{votable.class.name.underscore}";
+ var noteable_id = "#{votable.id}";
+ var aliases = #{AwardEmoji.aliases.to_json};
window.awards_handler = new AwardsHandler(
post_emoji_url,
noteable_type,
noteable_id,
aliases
- )
-
- $(".awards").on "click", ".emoji-menu-content li", (e) ->
- emoji = $(this).find(".emoji-icon").data("emoji")
- awards_handler.addAward(emoji)
-
- $(".awards").on "click", ".award", (e) ->
- emoji = $(this).find(".icon").data("emoji")
- awards_handler.addAward(emoji)
-
- $(".award").tooltip()
-
- $(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false})
+ );
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
new file mode 100644
index 00000000000..6ff361e4d80
--- /dev/null
+++ b/app/workers/delete_user_worker.rb
@@ -0,0 +1,10 @@
+class DeleteUserWorker
+ include Sidekiq::Worker
+
+ def perform(current_user_id, delete_user_id, options = {})
+ delete_user = User.find(delete_user_id)
+ current_user = User.find(current_user_id)
+
+ DeleteUserService.new(current_user).execute(delete_user, options.symbolize_keys)
+ end
+end
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 2d44d8d4dc6..605ec4f04e5 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -141,7 +141,7 @@ class IrkerWorker
end
def files_count(commit)
- files = "#{commit.diffs.count} file"
+ files = "#{commit.diffs.real_size} file"
files += 's' if commit.diffs.count > 1
files
end
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
new file mode 100644
index 00000000000..1b3232cd365
--- /dev/null
+++ b/app/workers/new_note_worker.rb
@@ -0,0 +1,12 @@
+class NewNoteWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform(note_id, note_params)
+ note = Note.find(note_id)
+
+ NotificationService.new.new_note(note)
+ Notes::PostProcessService.new(note).execute
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 994b8e8ed38..3cc232ef1ae 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -1,6 +1,5 @@
class PostReceive
include Sidekiq::Worker
- include Gitlab::Identifier
sidekiq_options queue: :post_receive
@@ -11,51 +10,44 @@ 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\z/, "")
- repo_path.gsub!(/\A\//, "")
+ post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes)
- project = Project.find_with_namespace(repo_path)
-
- if project.nil?
+ if post_received.project.nil?
log("Triggered hook for non-existing project with full path \"#{repo_path} \"")
return false
end
- changes = Base64.decode64(changes) unless changes.include?(" ")
- changes = utf8_encode_changes(changes)
- changes = changes.lines
+ if post_received.wiki?
+ # Nothing defined here yet.
+ elsif post_received.regular_project?
+ process_project_changes(post_received)
+ else
+ log("Triggered hook for unidentifiable repository type with full path \"#{repo_path} \"")
+ false
+ end
+ end
- changes.each do |change|
+ def process_project_changes(post_received)
+ post_received.changes.each do |change|
oldrev, newrev, ref = change.strip.split(' ')
- @user ||= identify(identifier, project, newrev)
+ @user ||= post_received.identify(newrev)
unless @user
- log("Triggered hook for non-existing user \"#{identifier} \"")
+ log("Triggered hook for non-existing user \"#{post_received.identifier} \"")
return false
end
if Gitlab::Git.tag_ref?(ref)
- GitTagPushService.new.execute(project, @user, oldrev, newrev, ref)
+ GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref)
else
- GitPushService.new.execute(project, @user, oldrev, newrev, ref)
+ GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
end
end
end
- def utf8_encode_changes(changes)
- changes = changes.dup
-
- changes.force_encoding("UTF-8")
- return changes if changes.valid_encoding?
-
- # 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]
-
- CharlockHolmes::Converter.convert(changes, detection[:encoding], 'UTF-8')
- end
-
+ private
+
def log(message)
Gitlab::GitLogger.error("POST-RECEIVE: #{message}")
end
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
new file mode 100644
index 00000000000..d06e4480292
--- /dev/null
+++ b/app/workers/project_destroy_worker.rb
@@ -0,0 +1,17 @@
+class ProjectDestroyWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform(project_id, user_id, params)
+ begin
+ project = Project.find(project_id)
+ rescue ActiveRecord::RecordNotFound
+ return
+ end
+
+ user = User.find(user_id)
+
+ ::Projects::DestroyService.new(project, user, params).execute
+ end
+end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 2f991c52339..21d311579e3 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -27,6 +27,7 @@ class RepositoryForkWorker
return
end
+ project.repository.after_import
project.import_finish
end
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index d18c0706b30..2937493c614 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -4,52 +4,21 @@ class RepositoryImportWorker
sidekiq_options queue: :gitlab_shell
- def perform(project_id)
- project = Project.find(project_id)
+ attr_accessor :project, :current_user
- if project.import_url == Project::UNKNOWN_IMPORT_URL
- # In this case, we only want to import issues, not a repository.
- unless project.create_repository
- project.update(import_error: "The repository could not be created.")
- project.import_fail
- return
- end
- else
- begin
- gitlab_shell.import_repository(project.path_with_namespace, project.import_url)
- rescue Gitlab::Shell::Error => e
- project.update(import_error: e.message)
- project.import_fail
- return
- end
- end
+ def perform(project_id)
+ @project = Project.find(project_id)
+ @current_user = @project.creator
- data_import_result =
- case project.import_type
- when 'github'
- Gitlab::GithubImport::Importer.new(project).execute
- when 'gitlab'
- Gitlab::GitlabImport::Importer.new(project).execute
- when 'bitbucket'
- Gitlab::BitbucketImport::Importer.new(project).execute
- when 'google_code'
- Gitlab::GoogleCodeImport::Importer.new(project).execute
- when 'fogbugz'
- Gitlab::FogbugzImport::Importer.new(project).execute
- else
- true
- end
+ result = Projects::ImportService.new(project, current_user).execute
- unless data_import_result
- project.update(import_error: "The remote issue data could not be imported.")
+ if result[:status] == :error
+ project.update(import_error: result[:message])
project.import_fail
return
end
- if project.import_type == 'bitbucket'
- Gitlab::BitbucketImport::KeyDeleter.new(project).execute
- end
-
+ project.repository.after_import
project.import_finish
end
end