summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Vosmaer <contact@jacobvosmaer.nl>2015-12-08 12:35:54 +0100
committerJacob Vosmaer <contact@jacobvosmaer.nl>2015-12-08 12:35:54 +0100
commit6d2be0212c444c6a3d25ae6a3c75822fa1c8614f (patch)
tree66c12a4f7863ded395300a56de689b5cb669e84b
parentad37f58ebd97fee3c06a531e4067c8adb4c9ecc7 (diff)
parentf5430e48b42227f1c1874ca27c6907f0f704be28 (diff)
downloadgitlab-ce-6d2be0212c444c6a3d25ae6a3c75822fa1c8614f.tar.gz
Merge branch 'master' into sync-all-repos
-rw-r--r--.flayignore1
-rw-r--r--.gitignore3
-rw-r--r--.gitlab-ci.yml31
-rw-r--r--.rubocop.yml4
-rw-r--r--.ruby-version2
-rw-r--r--CHANGELOG226
-rw-r--r--CHANGELOG-CI298
-rw-r--r--CONTRIBUTING.md33
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION1
-rw-r--r--Gemfile119
-rw-r--r--Gemfile.lock514
-rw-r--r--PROCESS.md23
-rw-r--r--Procfile6
-rw-r--r--README.md8
-rw-r--r--VERSION2
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Black.ttfbin148368 -> 289364 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttfbin0 -> 103404 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Bold.ttfbin148932 -> 291424 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttfbin0 -> 103608 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-ExtraLight.ttfbin150528 -> 291652 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttfbin0 -> 104768 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttfbin0 -> 104236 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Light.ttfbin150244 -> 293220 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttfbin0 -> 104616 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Regular.ttfbin149972 -> 293956 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Semibold.ttfbin149636 -> 292404 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttfbin0 -> 104020 bytes
-rw-r--r--app/assets/images/auth_buttons/facebook_64.pngbin0 -> 2970 bytes
-rw-r--r--app/assets/images/icon-link.pngbin726 -> 1128 bytes
-rw-r--r--app/assets/images/logo.svg16
-rw-r--r--app/assets/javascripts/application.js.coffee27
-rw-r--r--app/assets/javascripts/awards_handler.coffee96
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js.coffee29
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js.coffee3
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js.coffee16
-rw-r--r--app/assets/javascripts/blob/edit_blob.js.coffee8
-rw-r--r--app/assets/javascripts/blob/new_blob.js.coffee8
-rw-r--r--app/assets/javascripts/calendar.js.coffee2
-rw-r--r--app/assets/javascripts/ci/Chart.min.js39
-rw-r--r--app/assets/javascripts/ci/build.coffee4
-rw-r--r--app/assets/javascripts/ci/projects.js.coffee3
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js.coffee37
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee8
-rw-r--r--app/assets/javascripts/dropzone_input.js.coffee80
-rw-r--r--app/assets/javascripts/flash.js.coffee16
-rw-r--r--app/assets/javascripts/issues.js.coffee2
-rw-r--r--app/assets/javascripts/line_highlighter.js.coffee6
-rw-r--r--app/assets/javascripts/markdown_preview.js.coffee87
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee18
-rw-r--r--app/assets/javascripts/merge_request_widget.js.coffee16
-rw-r--r--app/assets/javascripts/new_commit_form.js.coffee21
-rw-r--r--app/assets/javascripts/notes.js.coffee43
-rw-r--r--app/assets/javascripts/project.js.coffee24
-rw-r--r--app/assets/javascripts/shortcuts_navigation.coffee1
-rw-r--r--app/assets/javascripts/sidebar.js.coffee1
-rw-r--r--app/assets/javascripts/stat_graph_contributors_util.js.coffee5
-rw-r--r--app/assets/javascripts/tree.js.coffee5
-rw-r--r--app/assets/javascripts/user.js.coffee6
-rw-r--r--app/assets/javascripts/users_select.js.coffee28
-rw-r--r--app/assets/stylesheets/application.scss50
-rw-r--r--app/assets/stylesheets/base/variables.scss43
-rw-r--r--app/assets/stylesheets/ci/runners.scss36
-rw-r--r--app/assets/stylesheets/framework.scss34
-rw-r--r--app/assets/stylesheets/framework/avatar.scss (renamed from app/assets/stylesheets/generic/avatar.scss)2
-rw-r--r--app/assets/stylesheets/framework/blocks.scss129
-rw-r--r--app/assets/stylesheets/framework/buttons.scss186
-rw-r--r--app/assets/stylesheets/framework/calendar.scss (renamed from app/assets/stylesheets/generic/calendar.scss)0
-rw-r--r--app/assets/stylesheets/framework/callout.scss (renamed from app/assets/stylesheets/generic/callout.scss)6
-rw-r--r--app/assets/stylesheets/framework/common.scss (renamed from app/assets/stylesheets/generic/common.scss)65
-rw-r--r--app/assets/stylesheets/framework/files.scss (renamed from app/assets/stylesheets/generic/files.scss)9
-rw-r--r--app/assets/stylesheets/framework/filters.scss (renamed from app/assets/stylesheets/generic/filters.scss)0
-rw-r--r--app/assets/stylesheets/framework/flash.scss (renamed from app/assets/stylesheets/generic/flash.scss)10
-rw-r--r--app/assets/stylesheets/framework/fonts.scss (renamed from app/assets/stylesheets/base/fonts.scss)0
-rw-r--r--app/assets/stylesheets/framework/forms.scss (renamed from app/assets/stylesheets/generic/forms.scss)41
-rw-r--r--app/assets/stylesheets/framework/gfm.scss (renamed from app/assets/stylesheets/generic/gfm.scss)1
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss (renamed from app/assets/stylesheets/themes/gitlab-theme.scss)0
-rw-r--r--app/assets/stylesheets/framework/header.scss (renamed from app/assets/stylesheets/generic/header.scss)20
-rw-r--r--app/assets/stylesheets/framework/highlight.scss (renamed from app/assets/stylesheets/generic/highlight.scss)0
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss (renamed from app/assets/stylesheets/generic/issue_box.scss)7
-rw-r--r--app/assets/stylesheets/framework/jquery.scss (renamed from app/assets/stylesheets/generic/jquery.scss)0
-rw-r--r--app/assets/stylesheets/framework/layout.scss (renamed from app/assets/stylesheets/base/layout.scss)11
-rw-r--r--app/assets/stylesheets/framework/lists.scss (renamed from app/assets/stylesheets/generic/lists.scss)13
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss (renamed from app/assets/stylesheets/generic/markdown_area.scss)8
-rw-r--r--app/assets/stylesheets/framework/mixins.scss (renamed from app/assets/stylesheets/base/mixins.scss)145
-rw-r--r--app/assets/stylesheets/framework/mobile.scss (renamed from app/assets/stylesheets/generic/mobile.scss)14
-rw-r--r--app/assets/stylesheets/framework/pagination.scss (renamed from app/assets/stylesheets/generic/pagination.scss)4
-rw-r--r--app/assets/stylesheets/framework/panels.scss15
-rw-r--r--app/assets/stylesheets/framework/selects.scss184
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss (renamed from app/assets/stylesheets/generic/sidebar.scss)180
-rw-r--r--app/assets/stylesheets/framework/tables.scss46
-rw-r--r--app/assets/stylesheets/framework/timeline.scss (renamed from app/assets/stylesheets/generic/timeline.scss)10
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss (renamed from app/assets/stylesheets/base/gl_bootstrap.scss)28
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss (renamed from app/assets/stylesheets/base/gl_variables.scss)6
-rw-r--r--app/assets/stylesheets/framework/typography.scss268
-rw-r--r--app/assets/stylesheets/framework/variables.scss99
-rw-r--r--app/assets/stylesheets/framework/zen.scss (renamed from app/assets/stylesheets/generic/zen.scss)0
-rw-r--r--app/assets/stylesheets/generic/blocks.scss62
-rw-r--r--app/assets/stylesheets/generic/buttons.scss90
-rw-r--r--app/assets/stylesheets/generic/selects.scss118
-rw-r--r--app/assets/stylesheets/generic/tables.scss20
-rw-r--r--app/assets/stylesheets/generic/typography.scss117
-rw-r--r--app/assets/stylesheets/highlight/dark.scss11
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss13
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss9
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss9
-rw-r--r--app/assets/stylesheets/highlight/white.scss15
-rw-r--r--app/assets/stylesheets/pages/builds.scss (renamed from app/assets/stylesheets/ci/builds.scss)11
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss (renamed from app/assets/stylesheets/ci/projects.scss)5
-rw-r--r--app/assets/stylesheets/pages/commit.scss34
-rw-r--r--app/assets/stylesheets/pages/commits.scss12
-rw-r--r--app/assets/stylesheets/pages/diff.scss115
-rw-r--r--app/assets/stylesheets/pages/editor.scss46
-rw-r--r--app/assets/stylesheets/pages/events.scss16
-rw-r--r--app/assets/stylesheets/pages/groups.scss11
-rw-r--r--app/assets/stylesheets/pages/help.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss115
-rw-r--r--app/assets/stylesheets/pages/issues.scss16
-rw-r--r--app/assets/stylesheets/pages/lint.scss (renamed from app/assets/stylesheets/ci/lint.scss)0
-rw-r--r--app/assets/stylesheets/pages/login.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss70
-rw-r--r--app/assets/stylesheets/pages/note_form.scss22
-rw-r--r--app/assets/stylesheets/pages/notes.scss7
-rw-r--r--app/assets/stylesheets/pages/profile.scss28
-rw-r--r--app/assets/stylesheets/pages/projects.scss293
-rw-r--r--app/assets/stylesheets/pages/runners.scss34
-rw-r--r--app/assets/stylesheets/pages/sherlock.scss33
-rw-r--r--app/assets/stylesheets/pages/snippets.scss26
-rw-r--r--app/assets/stylesheets/pages/status.scss (renamed from app/assets/stylesheets/ci/status.scss)0
-rw-r--r--app/assets/stylesheets/pages/tree.scss38
-rw-r--r--app/assets/stylesheets/pages/wiki.scss5
-rw-r--r--app/assets/stylesheets/pages/xterm.scss (renamed from app/assets/stylesheets/ci/xterm.scss)2
-rw-r--r--app/controllers/abuse_reports_controller.rb4
-rw-r--r--app/controllers/admin/application_controller.rb6
-rw-r--r--app/controllers/admin/application_settings_controller.rb4
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb2
-rw-r--r--app/controllers/admin/hooks_controller.rb2
-rw-r--r--app/controllers/admin/impersonation_controller.rb38
-rw-r--r--app/controllers/admin/services_controller.rb8
-rw-r--r--app/controllers/admin/users_controller.rb26
-rw-r--r--app/controllers/application_controller.rb40
-rw-r--r--app/controllers/autocomplete_controller.rb51
-rw-r--r--app/controllers/ci/admin/runners_controller.rb10
-rw-r--r--app/controllers/ci/application_controller.rb17
-rw-r--r--app/controllers/ci/builds_controller.rb78
-rw-r--r--app/controllers/ci/commits_controller.rb38
-rw-r--r--app/controllers/ci/events_controller.rb21
-rw-r--r--app/controllers/ci/lints_controller.rb4
-rw-r--r--app/controllers/ci/projects_controller.rb59
-rw-r--r--app/controllers/ci/runner_projects_controller.rb10
-rw-r--r--app/controllers/ci/runners_controller.rb73
-rw-r--r--app/controllers/ci/services_controller.rb59
-rw-r--r--app/controllers/ci/triggers_controller.rb43
-rw-r--r--app/controllers/ci/variables_controller.rb33
-rw-r--r--app/controllers/ci/web_hooks_controller.rb53
-rw-r--r--app/controllers/concerns/creates_merge_request_for_commit.rb28
-rw-r--r--app/controllers/concerns/global_milestones.rb21
-rw-r--r--app/controllers/concerns/issues_action.rb14
-rw-r--r--app/controllers/concerns/merge_requests_action.rb9
-rw-r--r--app/controllers/dashboard/milestones_controller.rb29
-rw-r--r--app/controllers/dashboard/projects_controller.rb1
-rw-r--r--app/controllers/dashboard_controller.rb25
-rw-r--r--app/controllers/groups/application_controller.rb11
-rw-r--r--app/controllers/groups/avatars_controller.rb4
-rw-r--r--app/controllers/groups/group_members_controller.rb35
-rw-r--r--app/controllers/groups/milestones_controller.rb63
-rw-r--r--app/controllers/groups_controller.rb28
-rw-r--r--app/controllers/import/bitbucket_controller.rb2
-rw-r--r--app/controllers/import/fogbugz_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb6
-rw-r--r--app/controllers/import/gitlab_controller.rb2
-rw-r--r--app/controllers/import/gitorious_controller.rb2
-rw-r--r--app/controllers/import/google_code_controller.rb8
-rw-r--r--app/controllers/invites_controller.rb4
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb4
-rw-r--r--app/controllers/passwords_controller.rb61
-rw-r--r--app/controllers/profiles/notifications_controller.rb2
-rw-r--r--app/controllers/profiles/preferences_controller.rb1
-rw-r--r--app/controllers/profiles_controller.rb3
-rw-r--r--app/controllers/projects/application_controller.rb6
-rw-r--r--app/controllers/projects/avatars_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb97
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/builds_controller.rb106
-rw-r--r--app/controllers/projects/ci_services_controller.rb49
-rw-r--r--app/controllers/projects/ci_settings_controller.rb36
-rw-r--r--app/controllers/projects/ci_web_hooks_controller.rb45
-rw-r--r--app/controllers/projects/commit_controller.rb53
-rw-r--r--app/controllers/projects/commits_controller.rb2
-rw-r--r--app/controllers/projects/compare_controller.rb8
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb2
-rw-r--r--app/controllers/projects/hooks_controller.rb7
-rw-r--r--app/controllers/projects/imports_controller.rb4
-rw-r--r--app/controllers/projects/issues_controller.rb13
-rw-r--r--app/controllers/projects/merge_requests_controller.rb13
-rw-r--r--app/controllers/projects/milestones_controller.rb6
-rw-r--r--app/controllers/projects/notes_controller.rb56
-rw-r--r--app/controllers/projects/project_members_controller.rb33
-rw-r--r--app/controllers/projects/raw_controller.rb2
-rw-r--r--app/controllers/projects/refs_controller.rb7
-rw-r--r--app/controllers/projects/releases_controller.rb31
-rw-r--r--app/controllers/projects/repositories_controller.rb17
-rw-r--r--app/controllers/projects/runners_controller.rb64
-rw-r--r--app/controllers/projects/services_controller.rb12
-rw-r--r--app/controllers/projects/snippets_controller.rb1
-rw-r--r--app/controllers/projects/tags_controller.rb22
-rw-r--r--app/controllers/projects/tree_controller.rb52
-rw-r--r--app/controllers/projects/triggers_controller.rb35
-rw-r--r--app/controllers/projects/uploads_controller.rb2
-rw-r--r--app/controllers/projects/variables_controller.rb25
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb40
-rw-r--r--app/controllers/root_controller.rb4
-rw-r--r--app/controllers/search_controller.rb4
-rw-r--r--app/controllers/sherlock/application_controller.rb12
-rw-r--r--app/controllers/sherlock/file_samples_controller.rb7
-rw-r--r--app/controllers/sherlock/queries_controller.rb7
-rw-r--r--app/controllers/sherlock/transactions_controller.rb19
-rw-r--r--app/controllers/snippets_controller.rb9
-rw-r--r--app/controllers/uploads_controller.rb6
-rw-r--r--app/controllers/users_controller.rb29
-rw-r--r--app/finders/contributed_projects_finder.rb37
-rw-r--r--app/finders/groups_finder.rb68
-rw-r--r--app/finders/issuable_finder.rb120
-rw-r--r--app/finders/joined_groups_finder.rb49
-rw-r--r--app/finders/milestones_finder.rb12
-rw-r--r--app/finders/notes_finder.rb4
-rw-r--r--app/finders/personal_projects_finder.rb41
-rw-r--r--app/finders/projects_finder.rb121
-rw-r--r--app/finders/trending_projects_finder.rb11
-rw-r--r--app/helpers/appearances_helper.rb2
-rw-r--r--app/helpers/application_helper.rb22
-rw-r--r--app/helpers/auth_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/button_helper.rb58
-rw-r--r--app/helpers/ci/application_helper.rb54
-rw-r--r--app/helpers/ci/builds_helper.rb19
-rw-r--r--app/helpers/ci/commits_helper.rb24
-rw-r--r--app/helpers/ci/gitlab_helper.rb25
-rw-r--r--app/helpers/ci/icons_helper.rb11
-rw-r--r--app/helpers/ci/runners_helper.rb22
-rw-r--r--app/helpers/ci/triggers_helper.rb7
-rw-r--r--app/helpers/ci/user_helper.rb15
-rw-r--r--app/helpers/ci_status_helper.rb44
-rw-r--r--app/helpers/commits_helper.rb4
-rw-r--r--app/helpers/diff_helper.rb60
-rw-r--r--app/helpers/emails_helper.rb2
-rw-r--r--app/helpers/events_helper.rb28
-rw-r--r--app/helpers/gitlab_markdown_helper.rb47
-rw-r--r--app/helpers/gitlab_routing_helper.rb14
-rw-r--r--app/helpers/icons_helper.rb22
-rw-r--r--app/helpers/issues_helper.rb50
-rw-r--r--app/helpers/labels_helper.rb15
-rw-r--r--app/helpers/merge_requests_helper.rb38
-rw-r--r--app/helpers/milestones_helper.rb7
-rw-r--r--app/helpers/namespaces_helper.rb15
-rw-r--r--app/helpers/nav_helper.rb8
-rw-r--r--app/helpers/notifications_helper.rb38
-rw-r--r--app/helpers/page_layout_helper.rb2
-rw-r--r--app/helpers/preferences_helper.rb19
-rw-r--r--app/helpers/projects_helper.rb28
-rw-r--r--app/helpers/runners_helper.rb29
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/selects_helper.rb30
-rw-r--r--app/helpers/tab_helper.rb18
-rw-r--r--app/helpers/time_helper.rb27
-rw-r--r--app/helpers/triggers_helper.rb5
-rw-r--r--app/helpers/visibility_level_helper.rb44
-rw-r--r--app/mailers/abuse_report_mailer.rb12
-rw-r--r--app/mailers/base_mailer.rb4
-rw-r--r--app/mailers/ci/notify.rb1
-rw-r--r--app/mailers/emails/issues.rb68
-rw-r--r--app/mailers/emails/notes.rb75
-rw-r--r--app/mailers/emails/projects.rb3
-rw-r--r--app/mailers/notify.rb2
-rw-r--r--app/models/ability.rb149
-rw-r--r--app/models/abuse_report.rb4
-rw-r--r--app/models/application_setting.rb29
-rw-r--r--app/models/broadcast_message.rb8
-rw-r--r--app/models/ci/application_setting.rb17
-rw-r--r--app/models/ci/build.rb196
-rw-r--r--app/models/ci/commit.rb220
-rw-r--r--app/models/ci/event.rb2
-rw-r--r--app/models/ci/project.rb93
-rw-r--r--app/models/ci/project_status.rb16
-rw-r--r--app/models/ci/runner.rb28
-rw-r--r--app/models/ci/runner_project.rb2
-rw-r--r--app/models/ci/service.rb2
-rw-r--r--app/models/ci/trigger.rb2
-rw-r--r--app/models/ci/trigger_request.rb2
-rw-r--r--app/models/ci/variable.rb2
-rw-r--r--app/models/ci/web_hook.rb5
-rw-r--r--app/models/commit.rb64
-rw-r--r--app/models/commit_range.rb110
-rw-r--r--app/models/commit_status.rb132
-rw-r--r--app/models/concerns/case_sensitivity.rb28
-rw-r--r--app/models/concerns/issuable.rb63
-rw-r--r--app/models/concerns/mentionable.rb53
-rw-r--r--app/models/concerns/participable.rb28
-rw-r--r--app/models/concerns/referable.rb23
-rw-r--r--app/models/concerns/sortable.rb11
-rw-r--r--app/models/concerns/strip_attribute.rb34
-rw-r--r--app/models/concerns/taskable.rb33
-rw-r--r--app/models/event.rb14
-rw-r--r--app/models/generic_commit_status.rb48
-rw-r--r--app/models/global_label.rb17
-rw-r--r--app/models/global_milestone.rb (renamed from app/models/group_milestone.rb)57
-rw-r--r--app/models/group.rb20
-rw-r--r--app/models/hooks/project_hook.rb25
-rw-r--r--app/models/hooks/service_hook.rb25
-rw-r--r--app/models/hooks/system_hook.rb25
-rw-r--r--app/models/hooks/web_hook.rb64
-rw-r--r--app/models/issue.rb14
-rw-r--r--app/models/label.rb10
-rw-r--r--app/models/lfs_object.rb8
-rw-r--r--app/models/lfs_objects_project.rb8
-rw-r--r--app/models/member.rb38
-rw-r--r--app/models/merge_request.rb47
-rw-r--r--app/models/merge_request_diff.rb31
-rw-r--r--app/models/milestone.rb41
-rw-r--r--app/models/namespace.rb21
-rw-r--r--app/models/note.rb114
-rw-r--r--app/models/project.rb123
-rw-r--r--app/models/project_services/bamboo_service.rb14
-rw-r--r--app/models/project_services/buildkite_service.rb2
-rw-r--r--app/models/project_services/ci/hip_chat_message.rb9
-rw-r--r--app/models/project_services/ci/hip_chat_service.rb4
-rw-r--r--app/models/project_services/ci/mail_service.rb12
-rw-r--r--app/models/project_services/ci/slack_message.rb23
-rw-r--r--app/models/project_services/ci/slack_service.rb4
-rw-r--r--app/models/project_services/drone_ci_service.rb34
-rw-r--r--app/models/project_services/external_wiki_service.rb6
-rw-r--r--app/models/project_services/gitlab_ci_service.rb54
-rw-r--r--app/models/project_services/hipchat_service.rb25
-rw-r--r--app/models/project_services/slack_service/note_message.rb31
-rw-r--r--app/models/project_services/teamcity_service.rb17
-rw-r--r--app/models/project_team.rb23
-rw-r--r--app/models/project_wiki.rb10
-rw-r--r--app/models/release.rb17
-rw-r--r--app/models/repository.rb214
-rw-r--r--app/models/sent_notification.rb7
-rw-r--r--app/models/service.rb30
-rw-r--r--app/models/snippet.rb4
-rw-r--r--app/models/user.rb167
-rw-r--r--app/models/users_star_project.rb2
-rw-r--r--app/services/archive_repository_service.rb47
-rw-r--r--app/services/ci/create_builds_service.rb39
-rw-r--r--app/services/ci/create_commit_service.rb34
-rw-r--r--app/services/ci/create_project_service.rb30
-rw-r--r--app/services/ci/create_trigger_request_service.rb13
-rw-r--r--app/services/ci/image_for_build_service.rb16
-rw-r--r--app/services/ci/register_build_service.rb8
-rw-r--r--app/services/ci/web_hook_service.rb3
-rw-r--r--app/services/compare_service.rb4
-rw-r--r--app/services/create_branch_service.rb5
-rw-r--r--app/services/create_release_service.rb31
-rw-r--r--app/services/create_tag_service.rb8
-rw-r--r--app/services/delete_branch_service.rb4
-rw-r--r--app/services/delete_tag_service.rb4
-rw-r--r--app/services/files/base_service.rb4
-rw-r--r--app/services/files/create_dir_service.rb20
-rw-r--r--app/services/files/create_service.rb13
-rw-r--r--app/services/files/update_service.rb2
-rw-r--r--app/services/git_hooks_service.rb28
-rw-r--r--app/services/git_push_service.rb67
-rw-r--r--app/services/gravatar_service.rb4
-rw-r--r--app/services/issuable_base_service.rb49
-rw-r--r--app/services/issues/create_service.rb2
-rw-r--r--app/services/issues/update_service.rb51
-rw-r--r--app/services/merge_requests/create_service.rb2
-rw-r--r--app/services/merge_requests/merge_service.rb6
-rw-r--r--app/services/merge_requests/post_merge_service.rb10
-rw-r--r--app/services/merge_requests/refresh_service.rb88
-rw-r--r--app/services/merge_requests/update_service.rb72
-rw-r--r--app/services/milestones/group_service.rb26
-rw-r--r--app/services/notes/create_service.rb12
-rw-r--r--app/services/notes/update_service.rb2
-rw-r--r--app/services/notification_service.rb135
-rw-r--r--app/services/projects/create_service.rb12
-rw-r--r--app/services/projects/fork_service.rb10
-rw-r--r--app/services/projects/transfer_service.rb6
-rw-r--r--app/services/system_hooks_service.rb71
-rw-r--r--app/services/system_note_service.rb47
-rw-r--r--app/services/update_release_service.rb29
-rw-r--r--app/uploaders/artifact_uploader.rb50
-rw-r--r--app/uploaders/attachment_uploader.rb19
-rw-r--r--app/uploaders/avatar_uploader.rb19
-rw-r--r--app/uploaders/file_uploader.rb21
-rw-r--r--app/uploaders/lfs_object_uploader.rb29
-rw-r--r--app/uploaders/uploader_helper.rb19
-rw-r--r--app/validators/color_validator.rb20
-rw-r--r--app/validators/email_validator.rb (renamed from lib/email_validator.rb)11
-rw-r--r--app/validators/line_code_validator.rb12
-rw-r--r--app/validators/namespace_name_validator.rb10
-rw-r--r--app/validators/namespace_validator.rb50
-rw-r--r--app/validators/url_validator.rb36
-rw-r--r--app/views/abuse_report_mailer/notify.html.haml11
-rw-r--r--app/views/abuse_report_mailer/notify.text.haml5
-rw-r--r--app/views/admin/abuse_reports/index.html.haml21
-rw-r--r--app/views/admin/application_settings/_form.html.haml17
-rw-r--r--app/views/admin/applications/index.html.haml2
-rw-r--r--app/views/admin/applications/show.html.haml37
-rw-r--r--app/views/admin/background_jobs/show.html.haml37
-rw-r--r--app/views/admin/deploy_keys/index.html.haml37
-rw-r--r--app/views/admin/identities/index.html.haml15
-rw-r--r--app/views/admin/labels/_form.html.haml6
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/labels/edit.html.haml8
-rw-r--r--app/views/admin/labels/index.html.haml4
-rw-r--r--app/views/admin/labels/new.html.haml6
-rw-r--r--app/views/admin/services/index.html.haml39
-rw-r--r--app/views/admin/users/_head.html.haml2
-rw-r--r--app/views/admin/users/_profile.html.haml (renamed from app/views/users/_profile.html.haml)4
-rw-r--r--app/views/admin/users/_projects.html.haml (renamed from app/views/users/_projects.html.haml)0
-rw-r--r--app/views/admin/users/edit.html.haml3
-rw-r--r--app/views/admin/users/index.html.haml16
-rw-r--r--app/views/admin/users/projects.html.haml2
-rw-r--r--app/views/admin/users/show.html.haml4
-rw-r--r--app/views/ci/admin/builds/_build.html.haml6
-rw-r--r--app/views/ci/admin/events/index.html.haml33
-rw-r--r--app/views/ci/admin/projects/_project.html.haml2
-rw-r--r--app/views/ci/admin/projects/index.html.haml23
-rw-r--r--app/views/ci/admin/runner_projects/index.html.haml2
-rw-r--r--app/views/ci/admin/runners/index.html.haml33
-rw-r--r--app/views/ci/admin/runners/show.html.haml40
-rw-r--r--app/views/ci/builds/_build.html.haml45
-rw-r--r--app/views/ci/builds/show.html.haml170
-rw-r--r--app/views/ci/commits/_commit.html.haml5
-rw-r--r--app/views/ci/commits/show.html.haml87
-rw-r--r--app/views/ci/events/index.html.haml19
-rw-r--r--app/views/ci/lints/_create.html.haml50
-rw-r--r--app/views/ci/lints/show.html.haml16
-rw-r--r--app/views/ci/notify/build_fail_email.html.haml10
-rw-r--r--app/views/ci/notify/build_fail_email.text.erb6
-rw-r--r--app/views/ci/notify/build_success_email.html.haml10
-rw-r--r--app/views/ci/notify/build_success_email.text.erb6
-rw-r--r--app/views/ci/projects/_info.html.haml2
-rw-r--r--app/views/ci/projects/disabled.html.haml1
-rw-r--r--app/views/ci/projects/edit.html.haml21
-rw-r--r--app/views/ci/projects/index.html.haml20
-rw-r--r--app/views/ci/projects/show.html.haml60
-rw-r--r--app/views/ci/runners/show.html.haml64
-rw-r--r--app/views/ci/services/edit.html.haml1
-rw-r--r--app/views/ci/shared/_guide.html.haml2
-rw-r--r--app/views/ci/user_sessions/new.html.haml3
-rw-r--r--app/views/dashboard/_activities.html.haml7
-rw-r--r--app/views/dashboard/_projects_head.html.haml3
-rw-r--r--app/views/dashboard/groups/index.html.haml6
-rw-r--r--app/views/dashboard/milestones/_issue.html.haml2
-rw-r--r--app/views/dashboard/milestones/_merge_request.html.haml2
-rw-r--r--app/views/dashboard/milestones/_milestone.html.haml11
-rw-r--r--app/views/dashboard/milestones/index.html.haml6
-rw-r--r--app/views/dashboard/milestones/show.html.haml120
-rw-r--r--app/views/dashboard/projects/_projects.html.haml7
-rw-r--r--app/views/dashboard/projects/index.atom.builder2
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/dashboard/projects/starred.html.haml5
-rw-r--r--app/views/dashboard/snippets/index.html.haml26
-rw-r--r--app/views/devise/passwords/new.html.haml2
-rw-r--r--app/views/doorkeeper/applications/index.html.haml30
-rw-r--r--app/views/doorkeeper/applications/show.html.haml38
-rw-r--r--app/views/doorkeeper/authorized_applications/index.html.haml25
-rw-r--r--app/views/events/_event_issue.atom.haml3
-rw-r--r--app/views/events/_event_merge_request.atom.haml3
-rw-r--r--app/views/events/_event_note.atom.haml2
-rw-r--r--app/views/events/_event_push.atom.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml4
-rw-r--r--app/views/explore/projects/index.html.haml2
-rw-r--r--app/views/explore/projects/starred.html.haml2
-rw-r--r--app/views/explore/projects/trending.html.haml2
-rw-r--r--app/views/explore/snippets/index.html.haml3
-rw-r--r--app/views/groups/_projects.html.haml9
-rw-r--r--app/views/groups/edit.html.haml15
-rw-r--r--app/views/groups/group_members/_group_member.html.haml12
-rw-r--r--app/views/groups/group_members/index.html.haml72
-rw-r--r--app/views/groups/group_members/update.js.haml2
-rw-r--r--app/views/groups/milestones/_issue.html.haml2
-rw-r--r--app/views/groups/milestones/_merge_request.html.haml2
-rw-r--r--app/views/groups/milestones/_milestone.html.haml2
-rw-r--r--app/views/groups/milestones/index.html.haml19
-rw-r--r--app/views/groups/milestones/new.html.haml47
-rw-r--r--app/views/groups/milestones/show.html.haml129
-rw-r--r--app/views/groups/new.html.haml8
-rw-r--r--app/views/groups/show.atom.builder2
-rw-r--r--app/views/groups/show.html.haml8
-rw-r--r--app/views/help/_shortcuts.html.haml14
-rw-r--r--app/views/help/ui.html.haml52
-rw-r--r--app/views/import/bitbucket/status.html.haml82
-rw-r--r--app/views/import/fogbugz/new.html.haml1
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml38
-rw-r--r--app/views/import/fogbugz/status.html.haml68
-rw-r--r--app/views/import/github/status.html.haml68
-rw-r--r--app/views/import/gitlab/status.html.haml68
-rw-r--r--app/views/import/gitorious/status.html.haml68
-rw-r--r--app/views/import/google_code/new.html.haml3
-rw-r--r--app/views/import/google_code/new_user_map.html.haml17
-rw-r--r--app/views/import/google_code/status.html.haml82
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml3
-rw-r--r--app/views/layouts/_page.html.haml13
-rw-r--r--app/views/layouts/_piwik.html.haml18
-rw-r--r--app/views/layouts/_search.html.haml8
-rw-r--r--app/views/layouts/admin.html.haml4
-rw-r--r--app/views/layouts/ci/_nav_admin.html.haml18
-rw-r--r--app/views/layouts/ci/_nav_project.html.haml37
-rw-r--r--app/views/layouts/ci/_page.html.haml10
-rw-r--r--app/views/layouts/ci/build.html.haml11
-rw-r--r--app/views/layouts/ci/commit.html.haml11
-rw-r--r--app/views/layouts/ci/project.html.haml11
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/layouts/errors.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml17
-rw-r--r--app/views/layouts/nav/_admin.html.haml28
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml18
-rw-r--r--app/views/layouts/nav/_explore.html.haml8
-rw-r--r--app/views/layouts/nav/_group.html.haml16
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml8
-rw-r--r--app/views/layouts/nav/_profile.html.haml22
-rw-r--r--app/views/layouts/nav/_project.html.haml53
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml45
-rw-r--r--app/views/layouts/notify.html.haml11
-rw-r--r--app/views/layouts/project.html.haml5
-rw-r--r--app/views/notify/_note_message.html.haml2
-rw-r--r--app/views/notify/merged_merge_request_email.text.haml2
-rw-r--r--app/views/notify/new_issue_email.html.haml2
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/project_was_moved_email.html.haml2
-rw-r--r--app/views/notify/project_was_moved_email.text.erb2
-rw-r--r--app/views/profiles/_event_table.html.haml29
-rw-r--r--app/views/profiles/accounts/show.html.haml22
-rw-r--r--app/views/profiles/applications.html.haml84
-rw-r--r--app/views/profiles/keys/_form.html.haml7
-rw-r--r--app/views/profiles/keys/_key_details.html.haml5
-rw-r--r--app/views/profiles/keys/_key_table.html.haml16
-rw-r--r--app/views/profiles/keys/index.html.haml4
-rw-r--r--app/views/profiles/notifications/_settings.html.haml2
-rw-r--r--app/views/profiles/notifications/show.html.haml6
-rw-r--r--app/views/profiles/preferences/show.html.haml11
-rw-r--r--app/views/profiles/preferences/update.js.erb7
-rw-r--r--app/views/profiles/show.html.haml12
-rw-r--r--app/views/projects/_activity.html.haml12
-rw-r--r--app/views/projects/_commit_button.html.haml4
-rw-r--r--app/views/projects/_files.html.haml6
-rw-r--r--app/views/projects/_home_panel.html.haml35
-rw-r--r--app/views/projects/_last_commit.html.haml12
-rw-r--r--app/views/projects/_md_preview.html.haml21
-rw-r--r--app/views/projects/_readme.html.haml38
-rw-r--r--app/views/projects/_zen.html.haml15
-rw-r--r--app/views/projects/activity.html.haml2
-rw-r--r--app/views/projects/blame/show.html.haml3
-rw-r--r--app/views/projects/blob/_actions.html.haml2
-rw-r--r--app/views/projects/blob/_blob.html.haml33
-rw-r--r--app/views/projects/blob/_editor.html.haml22
-rw-r--r--app/views/projects/blob/_new_dir.html.haml21
-rw-r--r--app/views/projects/blob/_remove.html.haml16
-rw-r--r--app/views/projects/blob/_upload.html.haml24
-rw-r--r--app/views/projects/blob/edit.html.haml17
-rw-r--r--app/views/projects/blob/new.html.haml22
-rw-r--r--app/views/projects/blob/show.html.haml7
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/branches/_commit.html.haml4
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/branches/new.html.haml16
-rw-r--r--app/views/projects/builds/_header_title.html.haml1
-rw-r--r--app/views/projects/builds/index.html.haml53
-rw-r--r--app/views/projects/builds/show.html.haml184
-rw-r--r--app/views/projects/buttons/_download.html.haml4
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml10
-rw-r--r--app/views/projects/buttons/_fork.html.haml26
-rw-r--r--app/views/projects/buttons/_notifications.html.haml32
-rw-r--r--app/views/projects/buttons/_star.html.haml19
-rw-r--r--app/views/projects/ci_services/_form.html.haml (renamed from app/views/ci/services/_form.html.haml)7
-rw-r--r--app/views/projects/ci_services/edit.html.haml2
-rw-r--r--app/views/projects/ci_services/index.html.haml (renamed from app/views/ci/services/index.html.haml)5
-rw-r--r--app/views/projects/ci_settings/_form.html.haml (renamed from app/views/ci/projects/_form.html.haml)44
-rw-r--r--app/views/projects/ci_settings/_no_runners.html.haml (renamed from app/views/ci/projects/_no_runners.html.haml)4
-rw-r--r--app/views/projects/ci_settings/edit.html.haml6
-rw-r--r--app/views/projects/ci_web_hooks/index.html.haml (renamed from app/views/ci/web_hooks/index.html.haml)28
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml9
-rw-r--r--app/views/projects/commit/_commit_box.html.haml13
-rw-r--r--app/views/projects/commit/builds.html.haml72
-rw-r--r--app/views/projects/commit/show.html.haml6
-rw-r--r--app/views/projects/commit_statuses/_commit_status.html.haml74
-rw-r--r--app/views/projects/commits/_commit.html.haml6
-rw-r--r--app/views/projects/commits/_head.html.haml2
-rw-r--r--app/views/projects/commits/show.atom.builder2
-rw-r--r--app/views/projects/compare/show.html.haml4
-rw-r--r--app/views/projects/deploy_keys/_form.html.haml9
-rw-r--r--app/views/projects/deploy_keys/new.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml17
-rw-r--r--app/views/projects/diffs/_file.html.haml50
-rw-r--r--app/views/projects/diffs/_stats.html.haml30
-rw-r--r--app/views/projects/edit.html.haml65
-rw-r--r--app/views/projects/empty.html.haml90
-rw-r--r--app/views/projects/forks/new.html.haml63
-rw-r--r--app/views/projects/graphs/_head.html.haml4
-rw-r--r--app/views/projects/graphs/ci.html.haml13
-rw-r--r--app/views/projects/graphs/ci/_build_times.haml17
-rw-r--r--app/views/projects/graphs/ci/_builds.haml50
-rw-r--r--app/views/projects/graphs/ci/_overall.haml16
-rw-r--r--app/views/projects/graphs/commits.html.haml60
-rw-r--r--app/views/projects/graphs/show.html.haml29
-rw-r--r--app/views/projects/hooks/index.html.haml2
-rw-r--r--app/views/projects/imports/new.html.haml25
-rw-r--r--app/views/projects/imports/show.html.haml2
-rw-r--r--app/views/projects/issues/_closed_by_box.html.haml3
-rw-r--r--app/views/projects/issues/_discussion.html.haml15
-rw-r--r--app/views/projects/issues/_form.html.haml8
-rw-r--r--app/views/projects/issues/_issue.html.haml28
-rw-r--r--app/views/projects/issues/_issues.html.haml7
-rw-r--r--app/views/projects/issues/edit.html.haml6
-rw-r--r--app/views/projects/issues/new.html.haml4
-rw-r--r--app/views/projects/issues/show.html.haml6
-rw-r--r--app/views/projects/labels/_form.html.haml10
-rw-r--r--app/views/projects/labels/_label.html.haml2
-rw-r--r--app/views/projects/labels/destroy.js.haml2
-rw-r--r--app/views/projects/labels/edit.html.haml8
-rw-r--r--app/views/projects/labels/index.html.haml7
-rw-r--r--app/views/projects/labels/new.html.haml6
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml15
-rw-r--r--app/views/projects/merge_requests/_form.html.haml3
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml42
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml6
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml31
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml23
-rw-r--r--app/views/projects/merge_requests/_show.html.haml19
-rw-r--r--app/views/projects/merge_requests/edit.html.haml2
-rw-r--r--app/views/projects/merge_requests/merge.js.haml2
-rw-r--r--app/views/projects/merge_requests/new.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml7
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml22
-rw-r--r--app/views/projects/merge_requests/widget/_merged.html.haml39
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml15
-rw-r--r--app/views/projects/merge_requests/widget/open/_check.html.haml8
-rw-r--r--app/views/projects/milestones/_form.html.haml18
-rw-r--r--app/views/projects/milestones/_issue.html.haml2
-rw-r--r--app/views/projects/milestones/_merge_request.html.haml2
-rw-r--r--app/views/projects/milestones/_milestone.html.haml8
-rw-r--r--app/views/projects/milestones/edit.html.haml6
-rw-r--r--app/views/projects/milestones/new.html.haml6
-rw-r--r--app/views/projects/milestones/show.html.haml115
-rw-r--r--app/views/projects/new.html.haml84
-rw-r--r--app/views/projects/notes/_edit_form.html.haml7
-rw-r--r--app/views/projects/notes/_form.html.haml13
-rw-r--r--app/views/projects/notes/_note.html.haml35
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml4
-rw-r--r--app/views/projects/project_members/_group_members.html.haml8
-rw-r--r--app/views/projects/project_members/_project_member.html.haml15
-rw-r--r--app/views/projects/project_members/_team.html.haml20
-rw-r--r--app/views/projects/project_members/index.html.haml44
-rw-r--r--app/views/projects/project_members/update.js.haml3
-rw-r--r--app/views/projects/protected_branches/_branches_list.html.haml59
-rw-r--r--app/views/projects/protected_branches/index.html.haml3
-rw-r--r--app/views/projects/releases/edit.html.haml19
-rw-r--r--app/views/projects/remove_fork.js.haml2
-rw-r--r--app/views/projects/runners/_runner.html.haml (renamed from app/views/ci/runners/_runner.html.haml)13
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml (renamed from app/views/ci/runners/_shared_runners.html.haml)8
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml (renamed from app/views/ci/runners/_specific_runners.html.haml)6
-rw-r--r--app/views/projects/runners/edit.html.haml (renamed from app/views/ci/runners/edit.html.haml)8
-rw-r--r--app/views/projects/runners/index.html.haml (renamed from app/views/ci/runners/index.html.haml)1
-rw-r--r--app/views/projects/runners/show.html.haml66
-rw-r--r--app/views/projects/services/_form.html.haml7
-rw-r--r--app/views/projects/services/index.html.haml39
-rw-r--r--app/views/projects/show.atom.builder2
-rw-r--r--app/views/projects/show.html.haml28
-rw-r--r--app/views/projects/snippets/_actions.html.haml11
-rw-r--r--app/views/projects/snippets/edit.html.haml2
-rw-r--r--app/views/projects/snippets/index.html.haml20
-rw-r--r--app/views/projects/snippets/new.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml46
-rw-r--r--app/views/projects/tags/_download.html.haml17
-rw-r--r--app/views/projects/tags/_tag.html.haml18
-rw-r--r--app/views/projects/tags/destroy.js.haml3
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tags/new.html.haml35
-rw-r--r--app/views/projects/tags/show.html.haml39
-rw-r--r--app/views/projects/tree/_blob_item.html.haml2
-rw-r--r--app/views/projects/tree/_readme.html.haml13
-rw-r--r--app/views/projects/tree/_tree.html.haml53
-rw-r--r--app/views/projects/tree/_tree_content.html.haml40
-rw-r--r--app/views/projects/tree/_tree_header.html.haml32
-rw-r--r--app/views/projects/tree/_tree_item.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml8
-rw-r--r--app/views/projects/triggers/_trigger.html.haml (renamed from app/views/ci/triggers/_trigger.html.haml)2
-rw-r--r--app/views/projects/triggers/index.html.haml (renamed from app/views/ci/triggers/index.html.haml)22
-rw-r--r--app/views/projects/variables/show.html.haml (renamed from app/views/ci/variables/show.html.haml)11
-rw-r--r--app/views/projects/wikis/_form.html.haml22
-rw-r--r--app/views/projects/wikis/_main_links.html.haml11
-rw-r--r--app/views/projects/wikis/_nav.html.haml25
-rw-r--r--app/views/projects/wikis/_new.html.haml4
-rw-r--r--app/views/projects/wikis/edit.html.haml24
-rw-r--r--app/views/projects/wikis/git_access.html.haml2
-rw-r--r--app/views/projects/wikis/history.html.haml49
-rw-r--r--app/views/projects/wikis/pages.html.haml6
-rw-r--r--app/views/projects/wikis/show.html.haml12
-rw-r--r--app/views/search/_category.html.haml7
-rw-r--r--app/views/search/_form.html.haml2
-rw-r--r--app/views/search/results/_commit.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml25
-rw-r--r--app/views/shared/_commit_message_container.html.haml8
-rw-r--r--app/views/shared/_confirm_modal.html.haml7
-rw-r--r--app/views/shared/_field.html.haml7
-rw-r--r--app/views/shared/_group_form.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml16
-rw-r--r--app/views/shared/_issues.html.haml7
-rw-r--r--app/views/shared/_logo.svg21
-rw-r--r--app/views/shared/_merge_requests.html.haml7
-rw-r--r--app/views/shared/_milestone_expired.html.haml5
-rw-r--r--app/views/shared/_new_commit_form.html.haml17
-rw-r--r--app/views/shared/_project_limit.html.haml8
-rw-r--r--app/views/shared/issuable/_context.html.haml33
-rw-r--r--app/views/shared/issuable/_filter.html.haml37
-rw-r--r--app/views/shared/issuable/_form.html.haml65
-rw-r--r--app/views/shared/issuable/_search_form.html.haml2
-rw-r--r--app/views/shared/projects/_list.html.haml7
-rw-r--r--app/views/shared/projects/_project.html.haml7
-rw-r--r--app/views/shared/snippets/_form.html.haml7
-rw-r--r--app/views/shared/snippets/_header.html.haml25
-rw-r--r--app/views/shared/snippets/_snippet.html.haml1
-rw-r--r--app/views/sherlock/file_samples/show.html.haml55
-rw-r--r--app/views/sherlock/queries/_backtrace.html.haml27
-rw-r--r--app/views/sherlock/queries/_general.html.haml50
-rw-r--r--app/views/sherlock/queries/show.html.haml26
-rw-r--r--app/views/sherlock/transactions/_file_samples.html.haml24
-rw-r--r--app/views/sherlock/transactions/_general.html.haml39
-rw-r--r--app/views/sherlock/transactions/_queries.html.haml24
-rw-r--r--app/views/sherlock/transactions/index.html.haml42
-rw-r--r--app/views/sherlock/transactions/show.html.haml36
-rw-r--r--app/views/snippets/_actions.html.haml11
-rw-r--r--app/views/snippets/edit.html.haml2
-rw-r--r--app/views/snippets/new.html.haml2
-rw-r--r--app/views/snippets/show.html.haml51
-rw-r--r--app/views/users/calendar.html.haml6
-rw-r--r--app/views/users/show.atom.builder2
-rw-r--r--app/views/users/show.html.haml152
-rw-r--r--app/views/votes/_votes_block.html.haml42
-rw-r--r--app/views/votes/_votes_inline.html.haml9
-rw-r--r--app/workers/email_receiver_worker.rb2
-rw-r--r--app/workers/emails_on_push_worker.rb2
-rw-r--r--app/workers/repository_archive_cache_worker.rb9
-rw-r--r--app/workers/repository_archive_worker.rb43
-rw-r--r--app/workers/repository_fork_worker.rb14
-rw-r--r--app/workers/repository_import_worker.rb62
-rw-r--r--app/workers/stuck_ci_builds_worker.rb18
-rwxr-xr-xbin/background_jobs2
-rwxr-xr-x[-rw-r--r--]bin/ci/upgrade.rb0
-rwxr-xr-xbin/rails6
-rwxr-xr-xbin/rake9
-rwxr-xr-xbin/setup29
-rwxr-xr-x[-rw-r--r--]bin/upgrade.rb0
-rw-r--r--config/application.rb6
-rw-r--r--config/environment.rb2
-rw-r--r--config/environments/development.rb4
-rw-r--r--config/environments/production.rb6
-rw-r--r--config/environments/test.rb8
-rw-r--r--config/gitlab.yml.example93
-rw-r--r--config/initializers/1_settings.rb62
-rw-r--r--config/initializers/2_app.rb6
-rw-r--r--config/initializers/active_record_query_trace.rb5
-rw-r--r--config/initializers/bullet.rb6
-rw-r--r--config/initializers/cookies_serializer.rb2
-rw-r--r--config/initializers/default_url_options.rb2
-rw-r--r--config/initializers/devise.rb4
-rw-r--r--config/initializers/inflections.rb21
-rw-r--r--config/initializers/rack_attack.rb.example14
-rw-r--r--config/initializers/rack_lineprof.rb31
-rw-r--r--config/initializers/rack_profiler.rb10
-rw-r--r--config/initializers/secret_token.rb8
-rw-r--r--config/initializers/session_store.rb25
-rw-r--r--config/initializers/sherlock.rb5
-rw-r--r--config/initializers/sidekiq.rb6
-rw-r--r--config/initializers/smtp_settings.rb.sample2
-rw-r--r--config/initializers/state_machine_patch.rb9
-rw-r--r--config/initializers/static_files.rb4
-rw-r--r--config/locales/devise.en.yml104
-rw-r--r--config/locales/sherlock.en.yml38
-rw-r--r--config/mail_room.yml39
-rw-r--r--config/mail_room.yml.example29
-rw-r--r--config/routes.rb132
-rw-r--r--config/schedule.yml10
-rw-r--r--db/fixtures/development/04_project.rb7
-rw-r--r--db/fixtures/development/05_users.rb4
-rw-r--r--db/fixtures/development/07_milestones.rb2
-rw-r--r--db/fixtures/development/09_issues.rb2
-rw-r--r--db/fixtures/development/12_snippets.rb2
-rw-r--r--db/migrate/20150924125150_add_project_id_to_ci_commit.rb5
-rw-r--r--db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb6
-rw-r--r--db/migrate/20150930001110_merge_request_error_field.rb5
-rw-r--r--db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb9
-rw-r--r--db/migrate/20151002112914_add_stage_idx_to_builds.rb5
-rw-r--r--db/migrate/20151002121400_add_index_for_builds.rb5
-rw-r--r--db/migrate/20151002122929_add_ref_and_tag_to_builds.rb6
-rw-r--r--db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb6
-rw-r--r--db/migrate/20151005075649_add_user_id_to_build.rb5
-rw-r--r--db/migrate/20151005150751_add_layout_option_for_users.rb5
-rw-r--r--db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb5
-rw-r--r--db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb17
-rw-r--r--db/migrate/20151008110232_add_users_lower_username_email_indexes.rb17
-rw-r--r--db/migrate/20151008123042_add_type_and_description_to_builds.rb9
-rw-r--r--db/migrate/20151008130321_migrate_name_to_description_for_builds.rb5
-rw-r--r--db/migrate/20151008143519_add_admin_notification_email_setting.rb5
-rw-r--r--db/migrate/20151013092124_add_artifacts_file_to_builds.rb5
-rw-r--r--db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb5
-rw-r--r--db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb9
-rw-r--r--db/migrate/20151016195706_add_notes_line_code_index.rb5
-rw-r--r--db/migrate/20151019111551_fix_build_tags.rb9
-rw-r--r--db/migrate/20151019111703_fail_build_without_names.rb8
-rw-r--r--db/migrate/20151020145526_add_services_template_index.rb5
-rw-r--r--db/migrate/20151020173516_ci_limits_to_mysql.rb9
-rw-r--r--db/migrate/20151020173906_add_ci_builds_index_for_status.rb5
-rw-r--r--db/migrate/20151023112551_fail_build_with_empty_name.rb8
-rw-r--r--db/migrate/20151023144219_remove_satellites.rb17
-rw-r--r--db/migrate/20151026182941_add_project_path_index.rb9
-rw-r--r--db/migrate/20151103001141_add_public_to_group.rb5
-rw-r--r--db/migrate/20151103133339_add_shared_runners_setting.rb5
-rw-r--r--db/migrate/20151103134857_create_lfs_objects.rb10
-rw-r--r--db/migrate/20151103134958_create_lfs_objects_projects.rb12
-rw-r--r--db/migrate/20151104105513_add_file_to_lfs_objects.rb5
-rw-r--r--db/migrate/20151105094515_create_releases.rb14
-rw-r--r--db/migrate/20151106000015_add_is_award_to_notes.rb6
-rw-r--r--db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb5
-rw-r--r--db/migrate/20151109134526_add_issues_state_index.rb5
-rw-r--r--db/migrate/20151109134916_add_projects_visibility_level_index.rb5
-rw-r--r--db/migrate/20151110125604_add_import_error_to_project.rb5
-rw-r--r--db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb6
-rw-r--r--db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb7
-rw-r--r--db/migrate/20151118162244_add_projects_public_index.rb5
-rw-r--r--db/migrate/20151203162133_add_hide_project_limit_to_users.rb5
-rw-r--r--db/migrate/limits_to_mysql.rb4
-rw-r--r--db/schema.rb178
-rw-r--r--doc/README.md30
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/commits.md90
-rw-r--r--doc/api/merge_requests.md61
-rw-r--r--doc/api/notes.md20
-rw-r--r--doc/api/projects.md77
-rw-r--r--doc/api/repositories.md74
-rw-r--r--doc/api/repository_files.md3
-rw-r--r--doc/api/tags.md131
-rw-r--r--doc/ci/api/README.md2
-rw-r--r--doc/ci/api/projects.md27
-rw-r--r--doc/ci/api/runners.md4
-rw-r--r--doc/ci/docker/using_docker_build.md9
-rw-r--r--doc/ci/docker/using_docker_images.md8
-rw-r--r--doc/ci/examples/README.md6
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md2
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md4
-rw-r--r--doc/ci/examples/test-clojure-application.md4
-rw-r--r--doc/ci/img/builds_tab.pngbin0 -> 3845 bytes
-rw-r--r--doc/ci/quick_start/README.md204
-rw-r--r--doc/ci/quick_start/build_status.pngbin62140 -> 0 bytes
-rw-r--r--doc/ci/quick_start/commit_status.pngbin33492 -> 0 bytes
-rw-r--r--doc/ci/quick_start/img/build_log.pngbin0 -> 63272 bytes
-rw-r--r--doc/ci/quick_start/img/builds_status.pngbin0 -> 49121 bytes
-rw-r--r--doc/ci/quick_start/img/new_commit.pngbin0 -> 9033 bytes
-rw-r--r--doc/ci/quick_start/img/runners_activated.pngbin0 -> 27597 bytes
-rw-r--r--doc/ci/quick_start/img/single_commit_status_pending.pngbin0 -> 36431 bytes
-rw-r--r--doc/ci/quick_start/img/status_pending.pngbin0 -> 19782 bytes
-rw-r--r--doc/ci/quick_start/new_commit.pngbin47527 -> 0 bytes
-rw-r--r--doc/ci/quick_start/projects.pngbin37014 -> 0 bytes
-rw-r--r--doc/ci/quick_start/runners.pngbin123048 -> 0 bytes
-rw-r--r--doc/ci/quick_start/runners_activated.pngbin60769 -> 0 bytes
-rw-r--r--doc/ci/variables/README.md38
-rw-r--r--doc/ci/yaml/README.md154
-rw-r--r--doc/customization/libravatar.md6
-rw-r--r--doc/customization/welcome_message.md30
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/benchmarking.md69
-rw-r--r--doc/development/profiling.md27
-rw-r--r--doc/development/rake_tasks.md2
-rw-r--r--doc/development/shared_files.md33
-rw-r--r--doc/development/shell_commands.md20
-rw-r--r--doc/gitlab-basics/README.md2
-rw-r--r--doc/gitlab-basics/create-issue.md27
-rw-r--r--doc/hooks/custom_hooks.md4
-rw-r--r--doc/incoming_email/README.md236
-rw-r--r--doc/install/database_mysql.md2
-rw-r--r--doc/install/installation.md56
-rw-r--r--doc/install/requirements.md6
-rw-r--r--doc/integration/facebook.md97
-rw-r--r--doc/integration/facebook_api_keys.pngbin0 -> 125921 bytes
-rw-r--r--doc/integration/facebook_app_settings.pngbin0 -> 134387 bytes
-rw-r--r--doc/integration/facebook_website_url.pngbin0 -> 42292 bytes
-rw-r--r--doc/integration/ldap.md24
-rw-r--r--doc/integration/omniauth.md5
-rw-r--r--doc/legal/corporate_contributor_license_agreement.md2
-rw-r--r--doc/legal/individual_contributor_license_agreement.md2
-rw-r--r--doc/markdown/markdown.md16
-rw-r--r--doc/migrate_ci_to_ce/README.md120
-rw-r--r--doc/operations/unicorn.md6
-rw-r--r--doc/permissions/permissions.md3
-rw-r--r--doc/public_access/public_access.md51
-rw-r--r--doc/raketasks/backup_restore.md22
-rw-r--r--doc/raketasks/cleanup.md3
-rw-r--r--doc/raketasks/user_management.md14
-rw-r--r--doc/release/README.md6
-rw-r--r--doc/release/monthly.md97
-rw-r--r--doc/release/patch.md40
-rw-r--r--doc/release/security.md4
-rw-r--r--doc/ssh/README.md8
-rw-r--r--doc/system_hooks/system_hooks.md42
-rw-r--r--doc/update/6.x-or-7.x-to-7.14.md2
-rw-r--r--doc/update/7.14-to-8.0.md15
-rw-r--r--doc/update/8.0-to-8.1.md171
-rw-r--r--doc/update/8.1-to-8.2.md189
-rw-r--r--doc/update/mysql_to_postgresql.md4
-rw-r--r--doc/update/patch_versions.md49
-rw-r--r--doc/web_hooks/web_hooks.md15
-rw-r--r--doc/workflow/README.md3
-rw-r--r--doc/workflow/award_emoji.pngbin0 -> 6620 bytes
-rw-r--r--doc/workflow/gitlab_flow.md23
-rw-r--r--doc/workflow/importing/import_projects_from_gitlab_com.md4
-rw-r--r--doc/workflow/importing/migrating_from_svn.md4
-rw-r--r--doc/workflow/lfs/lfs_administration.md41
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md126
-rw-r--r--doc/workflow/merge_requests.md12
-rw-r--r--doc/workflow/merge_requests/commit_compare.pngbin0 -> 89631 bytes
-rw-r--r--doc/workflow/merge_requests/merge_request_diff.pngbin0 -> 120422 bytes
-rw-r--r--doc/workflow/merge_requests/merge_request_diff_without_whitespace.pngbin0 -> 98887 bytes
-rw-r--r--doc/workflow/milestones.md13
-rw-r--r--doc/workflow/milestones/form.pngbin0 -> 88591 bytes
-rw-r--r--doc/workflow/milestones/group_form.pngbin0 -> 77087 bytes
-rw-r--r--doc/workflow/releases.md20
-rw-r--r--doc/workflow/releases/new_tag.pngbin0 -> 154755 bytes
-rw-r--r--doc/workflow/releases/tags.pngbin0 -> 165449 bytes
-rw-r--r--doc/workflow/voting_slider.pngbin5329 -> 0 bytes
-rw-r--r--doc_styleguide.md2
-rw-r--r--docker-compose.yml2
-rw-r--r--docker/.dockerignore1
-rw-r--r--docker/Dockerfile50
-rw-r--r--docker/README.md172
-rwxr-xr-xdocker/assets/wrapper21
-rw-r--r--docker/fig.yml2
-rw-r--r--docker/marathon.json31
-rw-r--r--docker/troubleshooting.md84
-rw-r--r--features/abuse_report.feature7
-rw-r--r--features/dashboard/new_project.feature10
-rw-r--r--features/group/members.feature105
-rw-r--r--features/group/milestones.feature30
-rw-r--r--features/groups.feature112
-rw-r--r--features/profile/profile.feature1
-rw-r--r--features/project/commits/commits.feature2
-rw-r--r--features/project/commits/diff_comments.feature6
-rw-r--r--features/project/commits/tags.feature20
-rw-r--r--features/project/issues/award_emoji.feature18
-rw-r--r--features/project/merge_requests.feature51
-rw-r--r--features/project/merge_requests/accept.feature19
-rw-r--r--features/project/project.feature6
-rw-r--r--features/project/service.feature1
-rw-r--r--features/project/snippets.feature2
-rw-r--r--features/project/source/browse_files.feature59
-rw-r--r--features/project/team_management.feature6
-rw-r--r--features/snippets/snippets.feature2
-rw-r--r--features/steps/abuse_reports.rb4
-rw-r--r--features/steps/admin/labels.rb14
-rw-r--r--features/steps/admin/projects.rb2
-rw-r--r--features/steps/dashboard/new_project.rb7
-rw-r--r--features/steps/group/members.rb147
-rw-r--r--features/steps/group/milestones.rb90
-rw-r--r--features/steps/groups.rb223
-rw-r--r--features/steps/profile/profile.rb15
-rw-r--r--features/steps/project/commits/commits.rb16
-rw-r--r--features/steps/project/commits/tags.rb36
-rw-r--r--features/steps/project/fork.rb3
-rw-r--r--features/steps/project/forked_merge_requests.rb9
-rw-r--r--features/steps/project/graph.rb8
-rw-r--r--features/steps/project/hooks.rb4
-rw-r--r--features/steps/project/issues/award_emoji.rb48
-rw-r--r--features/steps/project/issues/issues.rb12
-rw-r--r--features/steps/project/issues/labels.rb20
-rw-r--r--features/steps/project/issues/milestones.rb2
-rw-r--r--features/steps/project/merge_requests.rb71
-rw-r--r--features/steps/project/merge_requests/acceptance.rb39
-rw-r--r--features/steps/project/project.rb8
-rw-r--r--features/steps/project/redirects.rb2
-rw-r--r--features/steps/project/services.rb4
-rw-r--r--features/steps/project/snippets.rb8
-rw-r--r--features/steps/project/source/browse_files.rb103
-rw-r--r--features/steps/project/source/markdown_render.rb6
-rw-r--r--features/steps/project/team_management.rb4
-rw-r--r--features/steps/project/wiki.rb14
-rw-r--r--features/steps/shared/diff_note.rb17
-rw-r--r--features/steps/shared/group.rb6
-rw-r--r--features/steps/shared/paths.rb4
-rw-r--r--features/steps/shared/project.rb11
-rw-r--r--features/steps/shared/project_tab.rb2
-rw-r--r--features/steps/shared/user.rb4
-rw-r--r--features/steps/snippets/snippets.rb6
-rw-r--r--features/steps/snippets/user.rb6
-rw-r--r--lib/api/api.rb4
-rw-r--r--lib/api/commit_statuses.rb80
-rw-r--r--lib/api/entities.rb78
-rw-r--r--lib/api/files.rb4
-rw-r--r--lib/api/helpers.rb55
-rw-r--r--lib/api/merge_requests.rb30
-rw-r--r--lib/api/projects.rb18
-rw-r--r--lib/api/repositories.rb48
-rw-r--r--lib/api/services.rb2
-rw-r--r--lib/api/tags.rb86
-rw-r--r--lib/award_emoji.rb12
-rw-r--r--lib/backup/artifacts.rb13
-rw-r--r--lib/backup/builds.rb33
-rw-r--r--lib/backup/database.rb54
-rw-r--r--lib/backup/files.rb40
-rw-r--r--lib/backup/lfs.rb13
-rw-r--r--lib/backup/manager.rb21
-rw-r--r--lib/backup/repository.rb6
-rw-r--r--lib/backup/uploads.rb32
-rw-r--r--lib/ci/api/api.rb7
-rw-r--r--lib/ci/api/builds.rb102
-rw-r--r--lib/ci/api/commits.rb2
-rw-r--r--lib/ci/api/entities.rb11
-rw-r--r--lib/ci/api/helpers.rb21
-rw-r--r--lib/ci/api/projects.rb23
-rw-r--r--lib/ci/charts.rb3
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb155
-rw-r--r--lib/ci/migrate/builds.rb29
-rw-r--r--lib/ci/migrate/database.rb67
-rw-r--r--lib/ci/migrate/manager.rb72
-rw-r--r--lib/ci/migrate/tags.rb42
-rw-r--r--lib/ci/status.rb21
-rw-r--r--lib/event_filter.rb2
-rw-r--r--lib/extracts_path.rb2
-rw-r--r--lib/file_streamer.rb16
-rw-r--r--lib/gitlab/backend/grack_auth.rb23
-rw-r--r--lib/gitlab/backend/shell.rb10
-rw-r--r--lib/gitlab/blacklist.rb34
-rw-r--r--lib/gitlab/closing_issue_extractor.rb16
-rw-r--r--lib/gitlab/compare_result.rb4
-rw-r--r--lib/gitlab/contributions_calendar.rb1
-rw-r--r--lib/gitlab/current_settings.rb4
-rw-r--r--lib/gitlab/database.rb11
-rw-r--r--lib/gitlab/diff/file.rb8
-rw-r--r--lib/gitlab/diff/line.rb8
-rw-r--r--lib/gitlab/diff/parser.rb2
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb2
-rw-r--r--lib/gitlab/fogbugz_import/project_creator.rb2
-rw-r--r--lib/gitlab/force_push_check.rb2
-rw-r--r--lib/gitlab/git/hook.rb17
-rw-r--r--lib/gitlab/git_access.rb6
-rw-r--r--lib/gitlab/git_ref_validator.rb2
-rw-r--r--lib/gitlab/github_import/client.rb2
-rw-r--r--lib/gitlab/github_import/importer.rb2
-rw-r--r--lib/gitlab/gitlab_import/client.rb2
-rw-r--r--lib/gitlab/google_code_import/importer.rb93
-rw-r--r--lib/gitlab/google_code_import/project_creator.rb2
-rw-r--r--lib/gitlab/incoming_email.rb4
-rw-r--r--lib/gitlab/inline_diff.rb87
-rw-r--r--lib/gitlab/ldap/user.rb4
-rw-r--r--lib/gitlab/lfs/response.rb327
-rw-r--r--lib/gitlab/lfs/router.rb97
-rw-r--r--lib/gitlab/markdown.rb123
-rw-r--r--lib/gitlab/markdown/abstract_reference_filter.rb145
-rw-r--r--lib/gitlab/markdown/commit_range_reference_filter.rb84
-rw-r--r--lib/gitlab/markdown/commit_reference_filter.rb74
-rw-r--r--lib/gitlab/markdown/cross_project_reference.rb11
-rw-r--r--lib/gitlab/markdown/external_issue_reference_filter.rb13
-rw-r--r--lib/gitlab/markdown/external_link_filter.rb4
-rw-r--r--lib/gitlab/markdown/issue_reference_filter.rb61
-rw-r--r--lib/gitlab/markdown/label_reference_filter.rb27
-rw-r--r--lib/gitlab/markdown/merge_request_reference_filter.rb67
-rw-r--r--lib/gitlab/markdown/redactor_filter.rb43
-rw-r--r--lib/gitlab/markdown/reference_filter.rb145
-rw-r--r--lib/gitlab/markdown/reference_gatherer_filter.rb63
-rw-r--r--lib/gitlab/markdown/relative_link_filter.rb5
-rw-r--r--lib/gitlab/markdown/sanitization_filter.rb19
-rw-r--r--lib/gitlab/markdown/snippet_reference_filter.rb59
-rw-r--r--lib/gitlab/markdown/upload_link_filter.rb47
-rw-r--r--lib/gitlab/markdown/user_reference_filter.rb78
-rw-r--r--lib/gitlab/o_auth/provider.rb9
-rw-r--r--lib/gitlab/project_search_results.rb18
-rw-r--r--lib/gitlab/push_data_builder.rb5
-rw-r--r--lib/gitlab/reference_extractor.rb38
-rw-r--r--lib/gitlab/regex.rb17
-rw-r--r--lib/gitlab/seeder.rb2
-rw-r--r--lib/gitlab/sherlock.rb19
-rw-r--r--lib/gitlab/sherlock/collection.rb49
-rw-r--r--lib/gitlab/sherlock/file_sample.rb31
-rw-r--r--lib/gitlab/sherlock/line_profiler.rb98
-rw-r--r--lib/gitlab/sherlock/line_sample.rb36
-rw-r--r--lib/gitlab/sherlock/location.rb26
-rw-r--r--lib/gitlab/sherlock/middleware.rb41
-rw-r--r--lib/gitlab/sherlock/query.rb114
-rw-r--r--lib/gitlab/sherlock/transaction.rb136
-rw-r--r--lib/gitlab/sql/union.rb34
-rw-r--r--lib/gitlab/upgrader.rb8
-rw-r--r--lib/gitlab/uploads_transfer.rb35
-rwxr-xr-xlib/support/init.d/gitlab70
-rwxr-xr-xlib/support/init.d/gitlab.default.example11
-rw-r--r--lib/support/nginx/gitlab59
-rw-r--r--lib/support/nginx/gitlab-ssl61
-rw-r--r--lib/tasks/ci/migrate.rake87
-rw-r--r--lib/tasks/flay.rake9
-rw-r--r--lib/tasks/flog.rake25
-rw-r--r--lib/tasks/gitlab/backup.rake42
-rw-r--r--lib/tasks/gitlab/check.rake41
-rw-r--r--lib/tasks/gitlab/cleanup.rake49
-rw-r--r--lib/tasks/gitlab/setup.rake1
-rw-r--r--lib/tasks/gitlab/shell.rake10
-rw-r--r--lib/tasks/gitlab/task_helpers.rake12
-rw-r--r--lib/tasks/gitlab/two_factor.rake23
-rw-r--r--lib/tasks/grape.rake8
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake8
-rw-r--r--lib/tasks/spec.rake13
-rw-r--r--lib/tasks/spinach.rake6
-rw-r--r--lib/uploaded_file.rb37
-rw-r--r--public/robots.txt3
-rwxr-xr-xscripts/prepare_build.sh4
-rw-r--r--shared/.gitkeep (renamed from public/uploads/.gitkeep)0
-rw-r--r--shared/artifacts/.gitkeep (renamed from tmp/.gitkeep)0
-rw-r--r--shared/artifacts/tmp/cache/.gitkeep0
-rw-r--r--shared/artifacts/tmp/uploads/.gitkeep0
-rw-r--r--spec/benchmarks/finders/issues_finder_spec.rb55
-rw-r--r--spec/benchmarks/finders/trending_projects_finder_spec.rb14
-rw-r--r--spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb41
-rw-r--r--spec/benchmarks/models/milestone_spec.rb17
-rw-r--r--spec/benchmarks/models/project_spec.rb50
-rw-r--r--spec/benchmarks/models/project_team_spec.rb23
-rw-r--r--spec/benchmarks/models/user_spec.rb78
-rw-r--r--spec/benchmarks/services/projects/create_service_spec.rb28
-rw-r--r--spec/controllers/abuse_reports_controller_spec.rb76
-rw-r--r--spec/controllers/admin/impersonation_controller_spec.rb19
-rw-r--r--spec/controllers/admin/users_controller_spec.rb26
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb9
-rw-r--r--spec/controllers/ci/commits_controller_spec.rb27
-rw-r--r--spec/controllers/commit_controller_spec.rb35
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb27
-rw-r--r--spec/controllers/import/github_controller_spec.rb2
-rw-r--r--spec/controllers/invites_controller_spec.rb33
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb16
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb14
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb52
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb5
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb28
-rw-r--r--spec/controllers/projects/services_controller_spec.rb47
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb36
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb4
-rw-r--r--spec/controllers/projects_controller_spec.rb127
-rw-r--r--spec/controllers/root_controller_spec.rb24
-rw-r--r--spec/controllers/snippets_controller_spec.rb233
-rw-r--r--spec/controllers/users_controller_spec.rb23
-rw-r--r--spec/factories.rb12
-rw-r--r--spec/factories/ci/builds.rb7
-rw-r--r--spec/factories/ci/commits.rb56
-rw-r--r--spec/factories/ci/projects.rb24
-rw-r--r--spec/factories/commit_statuses.rb15
-rw-r--r--spec/factories/labels.rb1
-rw-r--r--spec/factories/lfs_objects.rb12
-rw-r--r--spec/factories/lfs_objects_projects.rb8
-rw-r--r--spec/factories/merge_requests.rb1
-rw-r--r--spec/factories/releases.rb21
-rw-r--r--spec/features/admin/admin_users_spec.rb64
-rw-r--r--spec/features/builds_spec.rb114
-rw-r--r--spec/features/ci/admin/builds_spec.rb11
-rw-r--r--spec/features/ci/admin/runners_spec.rb27
-rw-r--r--spec/features/ci/builds_spec.rb60
-rw-r--r--spec/features/ci/commits_spec.rb69
-rw-r--r--spec/features/ci/events_spec.rb22
-rw-r--r--spec/features/ci/projects_spec.rb38
-rw-r--r--spec/features/ci_settings_spec.rb22
-rw-r--r--spec/features/ci_web_hooks_spec.rb27
-rw-r--r--spec/features/commits_spec.rb75
-rw-r--r--spec/features/issues_spec.rb2
-rw-r--r--spec/features/login_spec.rb2
-rw-r--r--spec/features/markdown_spec.rb2
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb5
-rw-r--r--spec/features/password_reset_spec.rb62
-rw-r--r--spec/features/projects_spec.rb29
-rw-r--r--spec/features/runners_spec.rb (renamed from spec/features/ci/runners_spec.rb)35
-rw-r--r--spec/features/triggers_spec.rb (renamed from spec/features/ci/triggers_spec.rb)9
-rw-r--r--spec/features/users_spec.rb4
-rw-r--r--spec/features/variables_spec.rb (renamed from spec/features/ci/variables_spec.rb)15
-rw-r--r--spec/finders/contributed_projects_finder_spec.rb35
-rw-r--r--spec/finders/groups_finder_spec.rb48
-rw-r--r--spec/finders/issues_finder_spec.rb20
-rw-r--r--spec/finders/joined_groups_finder_spec.rb49
-rw-r--r--spec/finders/personal_projects_finder_spec.rb34
-rw-r--r--spec/finders/projects_finder_spec.rb82
-rw-r--r--spec/finders/trending_projects_finder_spec.rb39
-rw-r--r--spec/fixtures/markdown.md.erb17
-rw-r--r--spec/helpers/application_helper_spec.rb29
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb33
-rw-r--r--spec/helpers/issues_helper_spec.rb36
-rw-r--r--spec/helpers/merge_requests_helper.rb12
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb32
-rw-r--r--spec/helpers/preferences_helper_spec.rb8
-rw-r--r--spec/helpers/projects_helper_spec.rb18
-rw-r--r--spec/helpers/runners_helper_spec.rb (renamed from spec/helpers/ci/runners_helper_spec.rb)4
-rw-r--r--spec/helpers/search_helper_spec.rb5
-rw-r--r--spec/helpers/time_helper_spec.rb (renamed from spec/helpers/ci/application_helper_spec.rb)2
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js.coffee70
-rw-r--r--spec/javascripts/fixtures/behaviors/quick_submit.html.haml6
-rw-r--r--spec/javascripts/fixtures/line_highlighter.html.haml2
-rw-r--r--spec/javascripts/line_highlighter_spec.js.coffee2
-rw-r--r--spec/javascripts/spec_helper.coffee1
-rw-r--r--spec/lib/ci/charts_spec.rb5
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb490
-rw-r--r--spec/lib/gitlab/backend/grack_auth_spec.rb20
-rw-r--r--spec/lib/gitlab/backend/shell_spec.rb13
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb63
-rw-r--r--spec/lib/gitlab/database_spec.rb17
-rw-r--r--spec/lib/gitlab/email/attachment_uploader_spec.rb1
-rw-r--r--spec/lib/gitlab/inline_diff_spec.rb (renamed from spec/lib/gitlab/diff/inline_diff_spec.rb)0
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb21
-rw-r--r--spec/lib/gitlab/lfs/lfs_router_spec.rb765
-rw-r--r--spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb133
-rw-r--r--spec/lib/gitlab/markdown/commit_reference_filter_spec.rb110
-rw-r--r--spec/lib/gitlab/markdown/cross_project_reference_spec.rb36
-rw-r--r--spec/lib/gitlab/markdown/issue_reference_filter_spec.rb160
-rw-r--r--spec/lib/gitlab/markdown/label_reference_filter_spec.rb87
-rw-r--r--spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb104
-rw-r--r--spec/lib/gitlab/markdown/redactor_filter_spec.rb91
-rw-r--r--spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb89
-rw-r--r--spec/lib/gitlab/markdown/sanitization_filter_spec.rb101
-rw-r--r--spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb104
-rw-r--r--spec/lib/gitlab/markdown/upload_link_filter_spec.rb75
-rw-r--r--spec/lib/gitlab/markdown/user_reference_filter_spec.rb102
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb4
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/push_data_builder_spec.rb3
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb14
-rw-r--r--spec/lib/gitlab/sherlock/collection_spec.rb82
-rw-r--r--spec/lib/gitlab/sherlock/file_sample_spec.rb54
-rw-r--r--spec/lib/gitlab/sherlock/line_profiler_spec.rb73
-rw-r--r--spec/lib/gitlab/sherlock/line_sample_spec.rb33
-rw-r--r--spec/lib/gitlab/sherlock/location_spec.rb40
-rw-r--r--spec/lib/gitlab/sherlock/middleware_spec.rb79
-rw-r--r--spec/lib/gitlab/sherlock/query_spec.rb113
-rw-r--r--spec/lib/gitlab/sherlock/transaction_spec.rb235
-rw-r--r--spec/lib/gitlab/sql/union_spec.rb16
-rw-r--r--spec/lib/gitlab/uploads_transfer_spec.rb50
-rw-r--r--spec/lib/votes_spec.rb188
-rw-r--r--spec/mailers/ci/notify_spec.rb3
-rw-r--r--spec/mailers/notify_spec.rb82
-rw-r--r--spec/models/abuse_report_spec.rb12
-rw-r--r--spec/models/application_setting_spec.rb48
-rw-r--r--spec/models/broadcast_message_spec.rb19
-rw-r--r--spec/models/build_spec.rb (renamed from spec/models/ci/build_spec.rb)316
-rw-r--r--spec/models/ci/commit_spec.rb396
-rw-r--r--spec/models/ci/project_services/hip_chat_message_spec.rb85
-rw-r--r--spec/models/ci/project_services/hip_chat_service_spec.rb7
-rw-r--r--spec/models/ci/project_services/mail_service_spec.rb (renamed from spec/models/ci/mail_service_spec.rb)93
-rw-r--r--spec/models/ci/project_services/slack_message_spec.rb99
-rw-r--r--spec/models/ci/project_services/slack_service_spec.rb7
-rw-r--r--spec/models/ci/project_spec.rb143
-rw-r--r--spec/models/ci/runner_project_spec.rb2
-rw-r--r--spec/models/ci/runner_spec.rb69
-rw-r--r--spec/models/ci/service_spec.rb7
-rw-r--r--spec/models/ci/trigger_spec.rb12
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/ci/web_hook_spec.rb2
-rw-r--r--spec/models/commit_range_spec.rb127
-rw-r--r--spec/models/commit_spec.rb36
-rw-r--r--spec/models/commit_status_spec.rb197
-rw-r--r--spec/models/concerns/case_sensitivity_spec.rb189
-rw-r--r--spec/models/concerns/issuable_spec.rb1
-rw-r--r--spec/models/concerns/mentionable_spec.rb2
-rw-r--r--spec/models/concerns/strip_attribute_spec.rb20
-rw-r--r--spec/models/event_spec.rb38
-rw-r--r--spec/models/generic_commit_status_spec.rb72
-rw-r--r--spec/models/global_milestone_spec.rb65
-rw-r--r--spec/models/group_spec.rb47
-rw-r--r--spec/models/hooks/project_hook_spec.rb4
-rw-r--r--spec/models/hooks/service_hook_spec.rb2
-rw-r--r--spec/models/hooks/web_hook_spec.rb8
-rw-r--r--spec/models/issue_spec.rb39
-rw-r--r--spec/models/label_spec.rb1
-rw-r--r--spec/models/merge_request_spec.rb43
-rw-r--r--spec/models/milestone_spec.rb34
-rw-r--r--spec/models/namespace_spec.rb1
-rw-r--r--spec/models/note_spec.rb92
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb94
-rw-r--r--spec/models/project_services/gitlab_ci_service_spec.rb29
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb4
-rw-r--r--spec/models/project_services/jira_service_spec.rb6
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb93
-rw-r--r--spec/models/project_spec.rb91
-rw-r--r--spec/models/project_team_spec.rb12
-rw-r--r--spec/models/project_wiki_spec.rb22
-rw-r--r--spec/models/release_spec.rb28
-rw-r--r--spec/models/repository_spec.rb109
-rw-r--r--spec/models/service_spec.rb121
-rw-r--r--spec/models/user_spec.rb87
-rw-r--r--spec/models/wiki_page_spec.rb2
-rw-r--r--spec/requests/api/api_helpers_spec.rb26
-rw-r--r--spec/requests/api/commit_status_spec.rb137
-rw-r--r--spec/requests/api/commits_spec.rb13
-rw-r--r--spec/requests/api/files_spec.rb1
-rw-r--r--spec/requests/api/labels_spec.rb10
-rw-r--r--spec/requests/api/merge_requests_spec.rb22
-rw-r--r--spec/requests/api/projects_spec.rb99
-rw-r--r--spec/requests/api/repositories_spec.rb84
-rw-r--r--spec/requests/api/services_spec.rb34
-rw-r--r--spec/requests/api/tags_spec.rb196
-rw-r--r--spec/requests/api/users_spec.rb19
-rw-r--r--spec/requests/ci/api/builds_spec.rb225
-rw-r--r--spec/requests/ci/api/commits_spec.rb6
-rw-r--r--spec/requests/ci/api/projects_spec.rb112
-rw-r--r--spec/requests/ci/api/triggers_spec.rb19
-rw-r--r--spec/requests/ci/builds_spec.rb18
-rw-r--r--spec/requests/ci/commits_spec.rb17
-rw-r--r--spec/services/archive_repository_service_spec.rb69
-rw-r--r--spec/services/ci/create_commit_service_spec.rb89
-rw-r--r--spec/services/ci/create_project_service_spec.rb37
-rw-r--r--spec/services/ci/create_trigger_request_service_spec.rb34
-rw-r--r--spec/services/ci/event_service_spec.rb6
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb4
-rw-r--r--spec/services/ci/register_build_service_spec.rb15
-rw-r--r--spec/services/ci/web_hook_service_spec.rb3
-rw-r--r--spec/services/create_release_service_spec.rb34
-rw-r--r--spec/services/git_hooks_service_spec.rb53
-rw-r--r--spec/services/git_push_service_spec.rb10
-rw-r--r--spec/services/issues/close_service_spec.rb4
-rw-r--r--spec/services/issues/update_service_spec.rb100
-rw-r--r--spec/services/merge_requests/close_service_spec.rb4
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb19
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb40
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb4
-rw-r--r--spec/services/merge_requests/update_service_spec.rb68
-rw-r--r--spec/services/milestones/close_service_spec.rb28
-rw-r--r--spec/services/milestones/create_service_spec.rb24
-rw-r--r--spec/services/milestones/group_service_spec.rb70
-rw-r--r--spec/services/notes/create_service_spec.rb34
-rw-r--r--spec/services/notification_service_spec.rb350
-rw-r--r--spec/services/projects/create_service_spec.rb22
-rw-r--r--spec/services/projects/download_service_spec.rb2
-rw-r--r--spec/services/projects/fork_service_spec.rb29
-rw-r--r--spec/services/projects/transfer_service_spec.rb2
-rw-r--r--spec/services/projects/upload_service_spec.rb4
-rw-r--r--spec/services/system_hooks_service_spec.rb4
-rw-r--r--spec/services/system_note_service_spec.rb12
-rw-r--r--spec/services/update_release_service_spec.rb34
-rw-r--r--spec/spec_helper.rb8
-rw-r--r--spec/support/filter_spec_helper.rb36
-rw-r--r--spec/support/markdown_feature.rb34
-rw-r--r--spec/support/matchers/benchmark_matchers.rb61
-rw-r--r--spec/support/matchers/markdown_matchers.rb14
-rw-r--r--spec/support/mentionable_shared_examples.rb30
-rw-r--r--spec/support/services_shared_context.rb8
-rw-r--r--spec/support/setup_builds_storage.rb6
-rw-r--r--spec/support/stub_gitlab_calls.rb8
-rw-r--r--spec/support/test_env.rb13
-rw-r--r--spec/support/wait_for_ajax.rb11
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb34
-rw-r--r--spec/workers/email_receiver_worker_spec.rb14
-rw-r--r--spec/workers/repository_archive_worker_spec.rb79
-rw-r--r--spec/workers/repository_fork_worker_spec.rb1
-rw-r--r--spec/workers/stuck_ci_builds_worker_spec.rb44
-rw-r--r--vendor/assets/javascripts/clipboard.js621
1354 files changed, 28525 insertions, 13697 deletions
diff --git a/.flayignore b/.flayignore
new file mode 100644
index 00000000000..9c9875d4f9e
--- /dev/null
+++ b/.flayignore
@@ -0,0 +1 @@
+*.erb
diff --git a/.gitignore b/.gitignore
index 2a97eacad48..f5b6427ca03 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,7 +25,6 @@ config/initializers/rack_attack.rb
config/initializers/smtp_settings.rb
config/resque.yml
config/unicorn.rb
-config/mail_room.yml
config/secrets.yml
coverage/*
db/*.sqlite3
@@ -38,8 +37,10 @@ nohup.out
public/assets/
public/uploads.*
public/uploads/
+shared/artifacts/
rails_best_practices_output.html
/tags
tmp/
vendor/bundle/*
builds/*
+shared/*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ddf4e31204a..e8290fb36b2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -24,6 +24,14 @@ spec:api:
- ruby
- mysql
+spec:benchmark:
+ script:
+ - RAILS_ENV=test bundle exec rake spec:benchmark
+ tags:
+ - ruby
+ - mysql
+ allow_failure: true
+
spec:other:
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
@@ -65,3 +73,26 @@ brakeman:
tags:
- ruby
- mysql
+
+flog:
+ script:
+ - bundle exec rake flog
+ tags:
+ - ruby
+ - mysql
+
+flay:
+ script:
+ - bundle exec rake flay
+ tags:
+ - ruby
+ - mysql
+
+bundler:audit:
+ script:
+ - "bundle exec bundle-audit update"
+ - "bundle exec bundle-audit check"
+ tags:
+ - ruby
+ - mysql
+ allow_failure: true
diff --git a/.rubocop.yml b/.rubocop.yml
index 05b8ecc3b00..d59edbc8b17 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -888,7 +888,7 @@ Lint/RequireParentheses:
Lint/RescueException:
Description: 'Avoid rescuing the Exception class.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues'
- Enabled: false
+ Enabled: true
Lint/ShadowingOuterLocalVariable:
Description: >-
@@ -932,7 +932,7 @@ Lint/UselessAccessModifier:
Lint/UselessAssignment:
Description: 'Checks for useless assignment to a local variable.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
- Enabled: false
+ Enabled: true
Lint/UselessComparison:
Description: 'Checks for comparison of something with itself.'
diff --git a/.ruby-version b/.ruby-version
index 399088bf465..04b10b4f150 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.1.6
+2.1.7
diff --git a/CHANGELOG b/CHANGELOG
index 8ff61cd6e9f..bbd752fb1d0 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,19 +1,233 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.1.0 (unreleased)
- - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu)
- - Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu)
+v 8.3.0 (unreleased)
+ - Fix API setting of 'public' attribute to false will make a project private (Stan Hu)
+ - Handle and report SSL errors in Web hook test (Stan Hu)
+ - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera)
+ - Fix 500 error when update group member permission
+ - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera)
+ - Recognize issue/MR/snippet/commit links as references
+ - Add ignore whitespace change option to commit view
+ - Fire update hook from GitLab
+ - Style warning about mentioning many people in a comment
+ - Fix: sort milestones by due date once again (Greg Smethells)
+ - Don't show project fork event as "imported"
+ - Add API endpoint to fetch merge request commits list
+ - Expose events API with comment information and author info
+ - Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583
+ - Fix 500 error when creating a merge request that removes a submodule
+ - Run custom Git hooks when branch is created or deleted.
+ - Fix bug when simultaneously accepting multiple MRs results in MRs that are of "merged" status, but not merged to the target branch
+
+v 8.2.3
+ - Fix application settings cache not expiring after changes (Stan Hu)
+ - Fix Error 500s when creating global milestones with Unicode characters (Stan Hu)
+
+v 8.2.3
+ - Webhook payload has an added, modified and removed properties for each commit
+
+v 8.2.2
+ - Fix 404 in redirection after removing a project (Stan Hu)
+ - Ensure cached application settings are refreshed at startup (Stan Hu)
+ - Fix Error 500 when viewing user's personal projects from admin page (Stan Hu)
+ - Fix: Raw private snippets access workflow
+ - Prevent "413 Request entity too large" errors when pushing large files with LFS
+ - Fix invalid links within projects dashboard header
+ - Make current user the first user in assignee dropdown in issues detail page (Stan Hu)
+ - Fix: duplicate email notifications on issue comments
+
+v 8.2.1
+ - Forcefully update builds that didn't want to update with state machine
+ - Fix: saving GitLabCiService as Admin Template
+
+v 8.2.0
+ - Improved performance of finding projects and groups in various places
+ - Improved performance of rendering user profile pages and Atom feeds
+ - Expose build artifacts path as config option
+ - Fix grouping of contributors by email in graph.
+ - Improved performance of finding issues with/without labels
+ - Fix Drone CI service template not saving properly (Stan Hu)
+ - Fix avatars not showing in Atom feeds and project issues when Gravatar disabled (Stan Hu)
+ - Added a GitLab specific profiling tool called "Sherlock" (see GitLab CE merge request #1749)
+ - Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3 (Stan Hu)
+ - Improved performance of finding users by one of their Email addresses
+ - Add allow_failure field to commit status API (Stan Hu)
+ - Commits without .gitlab-ci.yml are marked as skipped
+ - Save detailed error when YAML syntax is invalid
+ - Since GitLab CI is enabled by default, remove enabling it by pushing .gitlab-ci.yml
+ - Added build artifacts
+ - Improved performance of replacing references in comments
+ - Show last project commit to default branch on project home page
+ - Highlight comment based on anchor in URL
+ - Adds ability to remove the forked relationship from project settings screen. (Han Loong Liauw)
+ - Improved performance of sorting milestone issues
+ - Allow users to select the Files view as default project view (Cristian Bica)
+ - Show "Empty Repository Page" for repository without branches (Artem V. Navrotskiy)
+ - Fix: Inability to reply to code comments in the MR view, if the MR comes from a fork
+ - Use git follow flag for commits page when retrieve history for file or directory
+ - Show merge request CI status on merge requests index page
+ - Send build name and stage in CI notification e-mail
+ - Extend yml syntax for only and except to support specifying repository path
+ - Enable shared runners to all new projects
+ - Bump GitLab-Workhorse to 0.4.1
+ - Allow to define cache in `.gitlab-ci.yml`
+ - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu)
+ - Remove deprecated CI events from project settings page
+ - Improve personal snippet access workflow (Douglas Alexandre)
+ - [API] Add ability to fetch the commit ID of the last commit that actually touched a file
+ - Fix omniauth documentation setting for omnibus configuration (Jon Cairns)
+ - Add "New file" link to dropdown on project page
+ - Include commit logs in project search
+ - Add "added", "modified" and "removed" properties to commit object in webhook
+ - Rename "Back to" links to "Go to" because its not always a case it point to place user come from
+ - Allow groups to appear in the search results if the group owner allows it
+ - Add email notification to former assignee upon unassignment (Adam Lieskovský)
+ - New design for project graphs page
+ - Remove deprecated dumped yaml file generated from previous job definitions
+ - Fix incoming email config defaults
+ - Show specific runners from projects where user is master or owner
+ - MR target branch is now visible on a list view when it is different from project's default one
+ - Improve Continuous Integration graphs page
+ - Make color of "Accept Merge Request" button consistent with current build status
+ - Add ignore white space option in merge request diff and commit and compare view
+ - Ability to add release notes (markdown text and attachments) to git tags (aka Releases)
+ - Relative links from a repositories README.md now link to the default branch
+ - Fix trailing whitespace issue in merge request/issue title
+ - Fix bug when milestone/label filter was empty for dashboard issues page
+ - Add ability to create milestone in group projects from single form
+ - Add option to create merge request when editing/creating a file (Dirceu Tiegs)
+ - Prevent the last owner of a group from being able to delete themselves by 'adding' themselves as a master (James Lopez)
+ - Add Award Emoji to issue and merge request pages
+
+v 8.1.4
+ - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu)
+ - Prevent redirect loop when home_page_url is set to the root URL
+ - Fix incoming email config defaults
+ - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu)
+
+v 8.1.3
+ - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu)
+ - Spread out runner contacted_at updates
+ - Use issue editor as cross reference comment author when issue is edited with a new mention
+ - Add Facebook authentication
+
+v 8.1.2
+ - Fix cloning Wiki repositories via HTTP (Stan Hu)
+ - Add migration to remove satellites directory
+ - Fix specific runners visibility
+ - Fix 500 when editing CI service
+ - Require CI jobs to be named
+ - Fix CSS for runner status
+ - Fix CI badge
+ - Allow developer to manage builds
+
+v 8.1.1
+ - Removed, see 8.1.2
+
+v 8.1.0
+ - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu)
+ - Fix duplicate repositories in GitHub import page (Stan Hu)
+ - Redirect to a default path if HTTP_REFERER is not set (Stan Hu)
+ - Adds ability to create directories using the web editor (Ben Ford)
+ - Cleanup stuck CI builds
+ - Send an email to admin email when a user is reported for spam (Jonathan Rochkind)
+ - Show notifications button when user is member of group rather than project (Grzegorz Bizon)
+ - Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge.
+ - Fix nonatomic database update potentially causing project star counts to go negative (Stan Hu)
+ - Don't show "Add README" link in an empty repository if user doesn't have access to push (Stan Hu)
+ - Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu)
+ - Speed up load times of issue detail pages by roughly 1.5x
+ - Fix CI rendering regressions
+ - If a merge request is to close an issue, show this on the issue page (Zeger-Jan van de Weg)
+ - Add a system note and update relevant merge requests when a branch is deleted or re-added (Stan Hu)
+ - Make diff file view easier to use on mobile screens (Stan Hu)
+ - Improved performance of finding users by username or Email address
+ - Fix bug where merge request comments created by API would not trigger notifications (Stan Hu)
+ - Add support for creating directories from Files page (Stan Hu)
+ - Allow removing of project without confirmation when JavaScript is disabled (Stan Hu)
+ - Support filtering by "Any" milestone or issue and fix "No Milestone" and "No Label" filters (Stan Hu)
+ - Improved performance of the trending projects page
+ - Remove CI migration task
+ - Improved performance of finding projects by their namespace
+ - Fix bug where transferring a project would result in stale commit links (Stan Hu)
+ - Fix build trace updating
+ - Include full path of source and target branch names in New Merge Request page (Stan Hu)
+ - Add user preference to view activities as default dashboard (Stan Hu)
+ - Add option to admin area to sign in as a specific user (Pavel Forkert)
- Show CI status on all pages where commits list is rendered
- Automatically enable CI when push .gitlab-ci.yml file to repository
- Move CI charts to project graphs area
- Fix cases where Markdown did not render links in activity feed (Stan Hu)
- Add first and last to pagination (Zeger-Jan van de Weg)
+ - Added Commit Status API
+ - Added Builds View
+ - Added when to .gitlab-ci.yml
- Show CI status on commit page
+ - Added CI_BUILD_TAG, _STAGE, _NAME and _TRIGGERED to CI builds
- Show CI status on Your projects page and Starred projects page
- Remove "Continuous Integration" page from dashboard
- Add notes and SSL verification entries to hook APIs (Ben Boeckel)
+ - Fix grammar in admin area "labels" .nothing-here-block when no labels exist.
+ - Move CI runners page to project settings area
+ - Move CI variables page to project settings area
+ - Move CI triggers page to project settings area
+ - Move CI project settings page to CE project settings area
+ - Fix bug when removed file was not appearing in merge request diff
+ - Show warning when build cannot be served by any of the available CI runners
+ - Note the original location of a moved project when notifying users of the move
+ - Improve error message when merging fails
+ - Add support of multibyte characters in LDAP UID (Roman Petrov)
+ - Show additions/deletions stats on merge request diff
+ - Remove footer text in emails (Zeger-Jan van de Weg)
+ - Ensure code blocks are properly highlighted after a note is updated
+ - Fix wrong access level badge on MR comments
+ - Hide password in the service settings form
+ - Move CI web hooks page to project settings area
+ - Fix User Identities API. It now allows you to properly create or update user's identities.
+ - Add user preference to change layout width (Peter Göbel)
+ - Use commit status in merge request widget as preferred source of CI status
+ - Integrate CI commit and build pages into project pages
+ - Move CI services page to project settings area
+ - Add "Quick Submit" behavior to input fields throughout the application. Use
+ Cmd+Enter on Mac and Ctrl+Enter on Windows/Linux.
+ - Fix position of hamburger in header for smaller screens (Han Loong Liauw)
+ - Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji)
+ - Persist filters when sorting on admin user page (Jerry Lukins)
+ - Update style of snippets pages (Han Loong Liauw)
+ - Allow dashboard and group issues/MRs to be filtered by label
+ - Add spellcheck=false to certain input fields
+ - Invalidate stored service password if the endpoint URL is changed
+ - Project names are not fully shown if group name is too big, even on group page view
+ - Apply new design for Files page
+ - Add "New Page" button to Wiki Pages tab (Stan Hu)
+ - Only render 404 page from /public
+ - Hide passwords from services API (Alex Lossent)
+ - Fix: Images cannot show when projects' path was changed
+ - Let gitlab-git-http-server generate and serve 'git archive' downloads
+ - Optimize query when filtering on issuables (Zeger-Jan van de Weg)
+ - Fix padding of outdated discussion item.
+ - Animate the logo on hover
+
+v 8.0.5
+ - Correct lookup-by-email for LDAP logins
+ - Fix loading spinner sometimes not being hidden on Merge Request tab switches
+
+v 8.0.4
+ - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu)
+ - Fix referrals for :back and relative URL installs
+ - Fix anchors to comments in diffs
+ - Remove CI token from build traces
+ - Fix "Assign All" button on Runner admin page
+ - Fix search in Files
+ - Add full project namespace to payload of system webhooks (Ricardo Band)
+
+v 8.0.3
+ - Fix URL shown in Slack notifications
+ - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu)
+ - Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu)
+ - Add work_in_progress key to MR web hooks (Ben Boeckel)
-v 8.0.2 (unreleased)
+v 8.0.2
- Fix default avatar not rendering in network graph (Stan Hu)
- Skip check_initd_configured_correctly on omnibus installs
- Prevent double-prefixing of help page paths
@@ -21,9 +235,11 @@ v 8.0.2 (unreleased)
- Make commit graphs responsive to window width changes (Stan Hu)
- Fix top margin for sign-in button on public pages
- Fix LDAP attribute mapping
+ - Remove git refs used internally by GitLab from network graph (Stan Hu)
- Use standard Markdown font in Markdown preview instead of fixed-width font (Stan Hu)
- Fix Reply by email for non-UTF-8 messages.
- Add option to use StartTLS with Reply by email IMAP server.
+ - Allow AWS S3 Server-Side Encryption with Amazon S3-Managed Keys for backups (Paul Beattie)
v 8.0.1
- Remove git refs used internally by GitLab from network graph (Stan Hu)
@@ -90,6 +306,8 @@ v 8.0.0
- Webhook for issue now contains repository field (Jungkook Park)
- Add ability to add custom text to the help page (Jeroen van Baarsen)
- Add pg_schema to backup config
+ - Fix references to target project issues in Merge Requests markdown preview and textareas (Francesco Levorato)
+ - Redirect from incorrectly cased group or project path to correct one (Francesco Levorato)
- Removed API calls from CE to CI
v 7.14.3
diff --git a/CHANGELOG-CI b/CHANGELOG-CI
deleted file mode 100644
index d1ad661d88b..00000000000
--- a/CHANGELOG-CI
+++ /dev/null
@@ -1,298 +0,0 @@
-v7.14.0 (unreleased)
- - Truncate commit messages after subject line in table
- - Adjust CI config to support Docker executors
- - Added Application Settings
- - Randomize test database for CI tests
- - Make YAML validation stricter
- - Use avatars received from GitLab
- - Refactor GitLab API usage to use either access_token or private_token depending on what was specified during login
- - Allow to use access_token for API requests
- - Fix project API listing returning empty list when first projects are not added to CI
- - Allow to define variables from YAML
- - Added support for CI skipped status
- - Fix broken yaml error saving
- - Add committed_at to commits to properly order last commit (the force push issue)
- - Rename type(s) to stage(s)
- - Fix navigation icons
- - Add missing stage when doing retry
- - Require variable keys to be not-empty and unique
- - Fix variable saving issue
- - Display variable saving errors in variables page not the project's
- - Added Build Triggers API
-
-v7.13.1
- - Fix: user could steal specific runner
- - Fix: don't send notifications for jobs with allow_failure set
- - Fix invalid link to doc.gitlab.com
-
-v7.13.0
- - Fix inline edit runner-description
- - Allow to specify image and services in yml that can be used with docker
- - Fix: No runner notification can see managers only
- - Fix service testing for slack
- - Ability to cancel all builds in commit at once
- - Disable colors in rake tasks automatically (if IO is not a TTY)
- - Implemented "rake env:info". Rake task to receive system information
- - Fix coverage calculation on commit page
- - Enhance YAML validation
- - Redirect back after authorization
- - Change favicon
- - Refactoring: Get rid of private_token usage in the frontend.
- - Allow to specify allow_failure for job
- - Build traces is stored in the file instead of database
- - Make the builds path configurable
- - Disable link to runner if it's not assigned to specific project
- - Store all secrets in config/secrets.yml
- - Encrypt variables
- - Allow to specify flexible list of types in yaml
-
-v7.12.2
- - Revert: Runner without tag should pick builds without tag only
-
-v7.12.1
- - Runner without tag should pick builds without tag only
- - Explicit error in the GitLab when commit not found.
- - Fix: lint with relative subpath
- - Update webhook example
- - Improved Lint stability
- - Add warning when .gitlab-ci.yml not found
- - Improved validation for .gitlab-ci.yml
- - Fix list of branches in only section
- - Fix "Status Badge" button
-
-v7.12.0
- - Endless scroll on the dashboard
- - Add notification if there are no runners
- - Fix pagination on dashboard
- - Remove ID column from runners list in the admin area
- - Increase default timeout for builds to 60 minutes
- - Using .gitlab-ci.yml file instead of jobs
- - Link to the runner from the build page for admin user
- - Ability to set secret variables for runner
- - Dont retry build when push same commit in same ref twice
- - Admin area: show amount of runners with last contact less than a minute ago
- - Fix re-adding project with the same name but different gitlab_id
- - Implementation of Lint (.gitlab-ci.yml validation tool)
- - Updated rails to 4.1.11
- - API fix: project create call
- - Link to web-editor with .gitlab-ci.yml
- - Updated examples in the documentation
-
-v7.11.0
- - Deploy Jobs API calls
- - Projects search on dashboard page
- - Improved runners page
- - Running and Pending tabs on admin builds page
- - Fix [ci skip] tag, so you can skip CI triggering now
- - Add HipChat notifications
- - Clean up project advanced settings.
- - Add a GitLab project path parameter to the project API
- - Remove projects IDs from dashboard
- - UI fix: Remove page headers from the admin area
- - Improve Email templates
- - Add backup/restore utility
- - Coordinator stores information(version, platform, revision, etc.) about runners.
- - Fixed pagination on dashboard
- - Public accessible build and commit pages of public projects
- - Fix vulnerability in the API when MySQL is used
-
-v7.10.1
- - Fix failing migration when update to 7.10 from 7.8 and older versions
-
-sidekiq_wirker_fix
- - added sidekiq.yml
- - integrated in script/background_jobs
-v7.10.0
- - Projects sorting by last commit date
- - Add project search at runner page
- - Fix GitLab and CI projects collision
- - Events for admin
- - Events per projects
- - Search for runners in admin area
- - UI improvements: created separated admin section, removed useless project show page
- - Runners sorting in admin area (by id)
- - Remove protected_attributes gem
- - Skip commit creation if there is no appropriate job
-
-v7.9.3
- - Contains no changes
- - Developers can cancel and retry jobs
-
-v7.9.2
- - [Security] Already existing projects should not be served by shared runners
- - Ability to run deploy job without test jobs (every push will trigger deploy job)
-
-v7.9.1
- - [Security] Adding explicit is_shared parameter to runner
- - [Security] By default new projects are not served by shared runners
-
-v7.9.0
- - Reset user session if token is invalid
- - Runner delete api endpoint
- - Fix bug about showing edit button on commit page if user does not have permissions
- - Allow to pass description and tag list during Runner's registration
- - Added api for project jobs
- - Implementation of deploy jobs after all parallel jobs(tests).
- - Add scroll up/down buttons for better mobile experience with large build traces
- - Add runner last contact (Kamil Trzciński)
- - Allow to pause runners - when paused runner will not receive any new build (Kamil Trzciński)
- - Add brakeman (security scanner for Ruby on Rails)
- - Changed a color of the canceled builds
- - Fix of show the same commits in different branches
-
-v7.8.2
- - Fix the broken build failed email
- - Notify only pusher instead of commiter
-
-v7.8.0
- - Fix OAuth login with GitLab installed in relative URL
- - GitLab CI has same version as GitLab since now
- - Allow to pass description and tag list during Runner's registration (Kamil Trzciński)
- - Update documentation (API, Install, Update)
- - Skip refs field supports for wildcard branch name (ex. feature/*)
- - Migrate E-mail notification to Services menu (Kamil Trzciński)
- - Added Slack notifications (Kamil Trzciński)
- - Disable turbolink on links pointing out to GitLab server
- - Add test coverage parsing example for pytest-cov
- - Upgrade raindrops gem
-
-v5.4.2
- - Fix exposure of project token via build data
-
-v5.4.1
- - Fix 500 if on builds page if build has no job
- - Truncate project token from build trace
- - Allow users with access to project see build trace
-
-v5.4.0 (Requires GitLab 7.7)
- - Fixed 500 error for badge if build is pending
- - Non-admin users can now register specific runners for their projects
- - Project specific runners page which users can access
- - Remove progress output from schedule_builds cron job
- - Fix schedule_builds rake task
- - Fix test webhook button
- - Job can be branch specific or tag specific or both
- - Shared runners builds projects which are not assigned to specific ones
- - Job can be runner specific through tags
- - Runner have tags
- - Move job settings to separate page
- - Add authorization level managing projects
- - OAuth authentication via GitLab.
-
-v5.3
- - Remove annoying 'Done' message from schedule_builds cron job
- - Fix a style issue with the navbar
- - Skip CSRF check on the project's build page
- - Fix showing wrong build script on admin projects page
- - Add branch and commit message to build result emails
-
-v5.2
- - Improve performance by adding new indicies
- - Separate Commit logic from Build logic in prep for Parallel Builds
- - Parallel builds
- - You can have multiple build scripts per project
-
-v5.1
- - Registration token and runner token are named differently
- - Redirect to previous page after sign-in
- - Dont show archived projects
- - Add support for skip branches from build
- - Add coverage parsing feature
- - Update rails to 4.0.10
- - Look for a REVISION file before running `git log`
- - All builds page for admin
-
-v5.0.1
- - Update rails to 4.0.5
-
-v5.0.0
- - Set build timeout in minutes
- - Web Hooks for builds
- - Nprogress bar
- - Remove extra spaces in build script
- - Requires runner v5
- * All script commands executed as one file
- * Cancel button works correctly now
- * Runner stability increased
- * Timeout applies to build now instead of line of script
-
-v4.3.0
- - Refactor build js
- - Redirect to build page with sha + bid if build id is not provided
- - Update rails to 4.0.3
- - Restyle project settings page
- - Improve help page
- - Replaced puma with unicorn
- - Improved init.d script
- - Add submodule init to default build script for new projects
-
-v4.2.0
- - Build duration chart
- - Bootstrap 3 with responsive UI
- - Improved init.d script
- - Refactoring
- - Changed http codes for POST /projects/:id/build action
- - Turbolinks
-
-v4.1.0
- - Rails 4
- - Click on build branch to see other builds for this branch
- - Email notifications (Jeroen Knoops)
-
-v4.0.0
- - Shared runners (no need to add runner to every project)
- - Admin area (only available for GitLab admins)
- - Hide all runners management into admin area
- - Use http cloning for builds instead of deploy keys
- - Allow choose between git clone and git fetch when get code for build
- - Make build timeout actually works
- - Requires GitLab 6.3 or higher
- - GitLab CI settings go to GitLab project via api on creation
-
-v3.2.0
- - Limit visibility of projects by gitlab authorized projects
- - Use one page for both gitlab and gitlab-ci projects
-
-v3.1.0
- - Login with both username, email or LDAP credentials (if GitLab 6.0+)
- - Retry build button functionality
- - UI fixes for resolution 1366px and lower
- - Fix gravatar ssl warning
-
-v3.0.0
- - Build running functionality extracted in gitlab-ci-runner
- - Added API for runners and builds
- - Redesigned application
- - Added charts
- - Use GitLab auth
- - Add projects via UI with few clicks
-
-v2.2.0
- - replaced unicorn with puma
- - replaced grit with rugged
- - Runner.rb more transactional safe now
- - updated rails to 3.2.13
- - updated devise to 2.2
- - fixed issue when build left in running status if exception triggered
- - rescue build timeout correctly
- - badge helper with markdown & html
- - increased test coverage to 85%
-
-v2.1.0
- - Removed horizontal scroll for build trace
- - new status badges
- - better encode
- - added several CI_* env variables
-
-v2.0.0
- - Replace resque with sidekiq
- - Run only one build at time per project
- - Added whenever for schedule jobs
-
-v1.2.0
- - Added Github web hook support
- - Added build schedule
-
-v1.1.0
- - Added JSON response for builds status
- - Compatible with GitLab v4.0.0 \ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 69abadb151a..7f5da063fd4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,7 +10,7 @@ By submitting code as an individual you agree to the [individual contributor lic
## Security vulnerability disclosure
-Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](http://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
+Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
## Closing policy for issues and merge requests
@@ -23,10 +23,20 @@ Issues and merge requests should be in English and contain appropriate language
## Helping others
Please help other GitLab users when you can.
-The channnels people will reach out on can be found on the [getting help page](https://about.gitlab.com/getting-help/).
-Sign up for the mailinglist, answer GitLab questions on StackOverflow or respond in the irc channel.
+The channels people will reach out on can be found on the [getting help page](https://about.gitlab.com/getting-help/).
+Sign up for the mailinglist, answer GitLab questions on StackOverflow or respond in the IRC channel.
You can also sign up on [CodeTriage](http://www.codetriage.com/gitlabhq/gitlabhq) to help with one issue every day.
+## I want to contribute!
+
+If you want to contribute to GitLab, but are not sure where to start,
+look for [issues](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=up-for-grabs)
+with the label `up-for-grabs`.
+These issues will be of reasonable size and challenge, for anyone to start
+contributing to GitLab.
+
+This was inspired by [an article by Kent C. Dodds](https://medium.com/@kentcdodds/first-timers-only-78281ea47455#.i2f363mx4).
+
## Issue tracker
To get support for your particular problem please use the [getting help channels](https://about.gitlab.com/getting-help/).
@@ -35,7 +45,7 @@ The [GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab
Do not use the issue tracker for feature requests. We have a specific [feature request forum](http://feedback.gitlab.com) for this purpose. Please keep feature requests as small and simple as possible, complex ones might be edited to make them small and simple.
-Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) or [Stack Overflow](http://stackoverflow.com/questions/tagged/gitlab) first. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there.
+Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) or [Stack Overflow](https://stackoverflow.com/questions/tagged/gitlab) first. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there.
### Issue tracker guidelines
@@ -47,10 +57,10 @@ Please send a merge request with a tested solution or a merge request with a fai
1. **Observed behavior**
1. **Relevant logs and/or screenshots:** Please use code blocks (\`\`\`) to format console output, logs, and code as it's very hard to read otherwise.
1. **Output of checks**
- * Results of GitLab [Application Check](doc/install/installation.md#check-application-status) (`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`); we will only investigate if the tests are passing
+ * Results of GitLab [Application Check](doc/install/installation.md#check-application-status) (For installations with omnibus-gitlab package: `sudo gitlab-rake gitlab:check SANITIZE=true`); For installations from source: `sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`); we will only investigate if the tests are passing
* Version of GitLab you are running; we will only investigate issues in the latest stable and development releases as per the [maintenance policy](MAINTENANCE.md)
* Add the last commit SHA-1 of the GitLab version you used to replicate the issue (obtainable from the help page)
- * Describe your setup (use relevant parts from `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
+ * Describe your setup (use relevant parts from the env info: For installations with omnibus-gitlab package: `sudo gitlab-rake gitlab:env:info`; For installations from source: `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
1. **Possible fixes**: If you can, link to the line of code that might be responsible for the problem
## Merge requests
@@ -59,7 +69,7 @@ We welcome merge requests with fixes and improvements to GitLab code, tests, and
Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) or [github.com](https://github.com/gitlabhq/gitlabhq/pulls).
-If you are new to GitLab development (or web development in general), search for the label `easyfix` ([gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [github](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint.
+If you are new to GitLab development (or web development in general), search for the label `easyfix` ([GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [GitHub](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint.
To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file.
@@ -72,7 +82,7 @@ If you can, please submit a merge request with the fix or improvements including
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
1. Add your changes to the [CHANGELOG](CHANGELOG)
1. If you are changing the README, some documentation or other things which have no effect on the tests, add `[ci skip]` somewhere in the commit message
-1. If you have multiple commits please combine them into one commit by [squashing them](http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
+1. If you have multiple commits please combine them into one commit by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
1. Push the commit to your fork
1. Submit a merge request (MR) to the master branch
1. The MR title should describe the change you want to make
@@ -83,6 +93,7 @@ If you can, please submit a merge request with the fix or improvements including
1. Be prepared to answer questions and incorporate feedback even if requests for this arrive weeks or months after your MR submission
1. If your MR touches code that executes shell commands, make sure it adheres to the [shell command guidelines]( doc/development/shell_commands.md).
1. Also have a look at the [shell command guidelines](doc/development/shell_commands.md) if your code reads or opens files, or handles paths to files on disk.
+1. If your code creates new files on disk please read the [shared files guidelines](doc/development/shared_files.md).
The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast.
Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as regressions requiring patch releases.
@@ -98,7 +109,7 @@ If you contribute to GitLab please know that changes involve more than just code
We have the following [definition of done](http://guide.agilealliance.org/guide/definition-of-done.html).
Please ensure you support the feature you contribute through all of these steps.
-1. Description explaning the relevancy (see following item)
+1. Description explaining the relevancy (see following item)
1. Working and clean code that is commented where needed
1. Unit and integration tests that pass on the CI server
1. Documented in the /doc directory
@@ -162,7 +173,7 @@ If you add a dependency in GitLab (such as an operating system package) please c
1. [Markdown](http://www.cirosantilli.com/markdown-styleguide)
1. [Database Migrations](doc/development/migration_style_guide.md)
1. [Documentation styleguide](doc_styleguide.md)
-1. Interface text should be written subjectively instead of objectively. It should be the gitlab core team addressing a person. It should be written in present time and never use past tense (has been/was). For example instead of "prohibited this user from being saved due to the following errors:" the text should be "sorry, we could not create your account because:". Also these [excellent writing guidelines](https://github.com/NARKOZ/guides#writing).
+1. Interface text should be written subjectively instead of objectively. It should be the GitLab core team addressing a person. It should be written in present time and never use past tense (has been/was). For example instead of "prohibited this user from being saved due to the following errors:" the text should be "sorry, we could not create your account because:". Also these [excellent writing guidelines](https://github.com/NARKOZ/guides#writing).
This is also the style used by linting tools such as [RuboCop](https://github.com/bbatsov/rubocop), [PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com).
@@ -180,4 +191,4 @@ This code of conduct applies both within project spaces and in public spaces whe
Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com
-This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
+This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 57cf282ebbc..743af5e1251 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.6.5
+2.6.8
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
new file mode 100644
index 00000000000..2b7c5ae0184
--- /dev/null
+++ b/GITLAB_WORKHORSE_VERSION
@@ -0,0 +1 @@
+0.4.2
diff --git a/Gemfile b/Gemfile
index 6950091b2b0..fc4d565fc84 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,14 +1,10 @@
source "https://rubygems.org"
-def darwin_only(require_as)
- RUBY_PLATFORM.include?('darwin') && require_as
-end
-
-def linux_only(require_as)
- RUBY_PLATFORM.include?('linux') && require_as
-end
+gem 'rails', '4.2.4'
+gem 'rails-deprecated_sanitizer', '~> 1.0.3'
-gem 'rails', '4.1.12'
+# Responders respond_to and respond_with
+gem 'responders', '~> 2.0'
# Specify a sprockets version due to security issue
# See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY
@@ -22,20 +18,21 @@ gem "mysql2", '~> 0.3.16', group: :mysql
gem "pg", '~> 0.18.2', group: :postgres
# Authentication libraries
-gem "devise", '~> 3.5.2'
-gem "devise-async", '~> 0.9.0'
-gem 'omniauth', "~> 1.2.2"
-gem 'omniauth-google-oauth2', '~> 0.2.5'
-gem 'omniauth-twitter', '~> 1.0.1'
-gem 'omniauth-github', '~> 1.1.1'
-gem 'omniauth-shibboleth', '~> 1.1.1'
-gem 'omniauth-kerberos', '~> 0.2.0', group: :kerberos
-gem 'omniauth-gitlab', '~> 1.0.0'
-gem 'omniauth-bitbucket', '~> 0.0.2'
-gem 'omniauth-saml', '~> 1.4.0'
-gem 'doorkeeper', '~> 2.1.3'
+gem 'devise', '~> 3.5.2'
+gem 'devise-async', '~> 0.9.0'
+gem 'doorkeeper', '~> 2.2.0'
+gem 'omniauth', '~> 1.2.2'
+gem 'omniauth-bitbucket', '~> 0.0.2'
+gem 'omniauth-facebook', '~> 3.0.0'
+gem 'omniauth-github', '~> 1.1.1'
+gem 'omniauth-gitlab', '~> 1.0.0'
+gem 'omniauth-google-oauth2', '~> 0.2.0'
+gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
+gem 'omniauth-saml', '~> 1.4.0'
+gem 'omniauth-shibboleth', '~> 1.2.0'
+gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd'
-gem "rack-oauth2", "~> 1.0.5"
+gem 'rack-oauth2', '~> 1.2.1'
# Two-factor authentication
gem 'devise-two-factor', '~> 2.0.0'
@@ -47,7 +44,7 @@ gem "browser", '~> 1.0.0'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 7.2.15'
+gem "gitlab_git", '~> 7.2.20'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -58,29 +55,22 @@ gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap"
gem 'gollum-lib', '~> 4.0.2'
# Language detection
-# GitLab fork of linguist does not require pygments/python dependency.
-# New version of original gem also dropped pygments support but it has strict
-# dependency to unstable rugged version. We have internal issue for replacing
-# fork with original gem when we meet on same rugged version - https://dev.gitlab.org/gitlab/gitlabhq/issues/2052.
-gem "gitlab-linguist", "~> 3.0.1", require: "linguist"
+gem "github-linguist", "~> 4.7.0", require: "linguist"
# API
-gem "grape", "~> 0.6.1"
-gem "grape-entity", "~> 0.4.2"
-gem 'rack-cors', '~> 0.2.9', require: 'rack/cors'
+gem 'grape', '~> 0.13.0'
+gem 'grape-entity', '~> 0.4.2'
+gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
# Format dates and times
# based on human-friendly examples
-gem "stamp", '~> 0.5.0'
-
-# Enumeration fields
-gem 'enumerize', '~> 0.7.0'
+gem "stamp", '~> 0.6.0'
# Pagination
gem "kaminari", "~> 0.16.3"
# HAML
-gem "haml-rails", '~> 0.5.3'
+gem "haml-rails", '~> 0.9.0'
# Files attachments
gem "carrierwave", '~> 0.9.0'
@@ -102,13 +92,14 @@ gem "seed-fu", '~> 2.3.5'
gem 'html-pipeline', '~> 1.11.0'
gem 'task_list', '~> 1.0.2', require: 'task_list/railtie'
gem 'github-markup', '~> 1.3.1'
-gem 'redcarpet', '~> 3.3.2'
+gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.2.9'
gem 'rdoc', '~>3.6'
gem 'org-ruby', '~> 0.9.12'
-gem 'creole', '~>0.3.6'
+gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
+gem 'rouge', '~> 1.10.1'
# Diffs
gem 'diffy', '~> 3.0.3'
@@ -120,7 +111,7 @@ group :unicorn do
end
# State machine
-gem "state_machine", '~> 1.2.0'
+gem "state_machines-activerecord", '~> 0.3.0'
# Run events after state machine commits
gem 'after_commit_queue'
@@ -128,17 +119,15 @@ gem 'after_commit_queue'
gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs
-gem 'slim', '~> 2.0.2'
gem 'sinatra', '~> 1.4.4', require: nil
-gem 'sidekiq', '3.3.0'
-gem 'sidetiq', '~> 0.6.3'
+gem 'sidekiq', '~> 3.5.0'
+gem 'sidekiq-cron', '~> 0.3.0'
# HTTP requests
gem "httparty", '~> 0.13.3'
# Colored output to console
-gem "colored", '~> 1.2'
-gem "colorize", '~> 0.5.8'
+gem "colorize", '~> 0.7.0'
# GitLab settings
gem 'settingslogic', '~> 2.0.9'
@@ -151,7 +140,7 @@ gem 'version_sorter', '~> 2.0.0'
gem "redis-rails", '~> 4.0.0'
# Campfire integration
-gem 'tinder', '~> 1.9.2'
+gem 'tinder', '~> 1.10.0'
# HipChat integration
gem 'hipchat', '~> 1.5.0'
@@ -163,10 +152,10 @@ gem "gitlab-flowdock-git-hook", "~> 1.0.1"
gem "gemnasium-gitlab-service", "~> 0.2"
# Slack integration
-gem "slack-notifier", "~> 1.0.0"
+gem "slack-notifier", "~> 1.2.0"
# Asana integration
-gem 'asana', '~> 0.0.6'
+gem 'asana', '~> 0.4.0'
# FogBugz integration
gem 'ruby-fogbugz', '~> 0.2.1'
@@ -182,6 +171,7 @@ gem "underscore-rails", "~> 1.4.4"
# Sanitize user input
gem "sanitize", '~> 2.0'
+gem 'babosa', '~> 1.0.2'
# Protect against bruteforcing
gem "rack-attack", '~> 4.3.0'
@@ -193,28 +183,29 @@ gem 'ace-rails-ap', '~> 2.0.1'
gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
-gem 'charlock_holmes', '~> 0.6.9.4'
+gem 'charlock_holmes', '~> 0.7.3'
gem "sass-rails", '~> 4.0.5'
gem "coffee-rails", '~> 4.1.0'
-gem "uglifier", '~> 2.3.2'
+gem "uglifier", '~> 2.7.2'
gem 'turbolinks', '~> 2.5.0'
-gem 'jquery-turbolinks', '~> 2.0.1'
+gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.0'
gem 'font-awesome-rails', '~> 4.2'
gem 'gitlab_emoji', '~> 0.1'
-gem 'gon', '~> 5.0.0'
-gem 'jquery-atwho-rails', '~> 1.0.0'
+gem 'gon', '~> 6.0.1'
+gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 3.1.3'
gem 'jquery-scrollto-rails', '~> 1.4.3'
gem 'jquery-ui-rails', '~> 4.2.1'
-gem 'nprogress-rails', '~> 0.1.2.3'
+gem 'nprogress-rails', '~> 0.1.6.7'
gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.2.0'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
+gem 'net-ssh', '~> 3.0.1'
group :development do
gem "foreman"
@@ -223,8 +214,10 @@ group :development do
gem "annotate", "~> 2.6.0"
gem "letter_opener", '~> 1.1.2'
gem 'quiet_assets', '~> 1.0.2'
- gem 'rack-mini-profiler', '~> 0.9.0', require: false
gem 'rerun', '~> 0.10.0'
+ gem 'bullet', require: false
+ gem 'rblineprof', platform: :mri, require: false
+ gem 'web-console', '~> 2.0'
# Better errors handler
gem 'better_errors', '~> 1.0.1'
@@ -270,13 +263,18 @@ group :development, :test do
gem 'rubocop', '~> 0.28.0', require: false
gem 'coveralls', '~> 0.8.2', require: false
gem 'simplecov', '~> 0.10.0', require: false
+ gem 'flog', require: false
+ gem 'flay', require: false
+ gem 'bundler-audit', require: false
+
+ gem 'benchmark-ips', require: false
end
group :test do
gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0'
gem 'webmock', '~> 1.21.0'
- gem 'test_after_commit', '~> 0.2.2'
+ gem 'test_after_commit', '~> 0.4.2'
gem 'sham_rack'
end
@@ -289,7 +287,7 @@ gem 'newrelic-grape'
gem 'octokit', '~> 3.7.0'
-gem "mail_room", "~> 0.5.2"
+gem "mail_room", "~> 0.6.1"
gem 'email_reply_parser', '~> 0.5.8'
@@ -298,19 +296,8 @@ gem 'activerecord-deprecated_finders', '~> 1.0.3'
gem 'activerecord-session_store', '~> 0.1.0'
gem "nested_form", '~> 0.3.2'
-# Scheduled
-gem 'whenever', '~> 0.8.4', require: false
-
# OAuth
gem 'oauth2', '~> 1.0.0'
# Soft deletion
gem "paranoia", "~> 2.0"
-
-group :development, :test do
- gem 'guard-rspec', '~> 4.2.0'
-
- gem 'rb-fsevent', require: darwin_only('rb-fsevent')
- gem 'growl', require: darwin_only('growl')
- gem 'rb-inotify', require: linux_only('rb-inotify')
-end
diff --git a/Gemfile.lock b/Gemfile.lock
index 4386c6b9abb..5d70788d981 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,63 +1,71 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (2.3.1)
+ CFPropertyList (2.3.2)
RedCloth (4.2.9)
ace-rails-ap (2.0.1)
- actionmailer (4.1.12)
- actionpack (= 4.1.12)
- actionview (= 4.1.12)
+ actionmailer (4.2.4)
+ actionpack (= 4.2.4)
+ actionview (= 4.2.4)
+ activejob (= 4.2.4)
mail (~> 2.5, >= 2.5.4)
- actionpack (4.1.12)
- actionview (= 4.1.12)
- activesupport (= 4.1.12)
- rack (~> 1.5.2)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ actionpack (4.2.4)
+ actionview (= 4.2.4)
+ activesupport (= 4.2.4)
+ rack (~> 1.6)
rack-test (~> 0.6.2)
- actionview (4.1.12)
- activesupport (= 4.1.12)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ actionview (4.2.4)
+ activesupport (= 4.2.4)
builder (~> 3.1)
erubis (~> 2.7.0)
- activemodel (4.1.12)
- activesupport (= 4.1.12)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ activejob (4.2.4)
+ activesupport (= 4.2.4)
+ globalid (>= 0.3.0)
+ activemodel (4.2.4)
+ activesupport (= 4.2.4)
builder (~> 3.1)
- activerecord (4.1.12)
- activemodel (= 4.1.12)
- activesupport (= 4.1.12)
- arel (~> 5.0.0)
+ activerecord (4.2.4)
+ activemodel (= 4.2.4)
+ activesupport (= 4.2.4)
+ arel (~> 6.0)
activerecord-deprecated_finders (1.0.4)
- activerecord-session_store (0.1.1)
+ activerecord-session_store (0.1.2)
actionpack (>= 4.0.0, < 5)
activerecord (>= 4.0.0, < 5)
railties (>= 4.0.0, < 5)
- activeresource (4.0.0)
- activemodel (~> 4.0)
- activesupport (~> 4.0)
- rails-observers (~> 0.1.1)
- activesupport (4.1.12)
- i18n (~> 0.6, >= 0.6.9)
+ activesupport (4.2.4)
+ i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
- thread_safe (~> 0.1)
+ thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
acts-as-taggable-on (3.5.0)
activerecord (>= 3.2, < 5)
addressable (2.3.8)
- after_commit_queue (1.1.0)
- rails (>= 3.0)
+ after_commit_queue (1.3.0)
+ activerecord (>= 3.0)
annotate (2.6.10)
activerecord (>= 3.2, <= 4.3)
rake (~> 10.4)
- arel (5.0.1.20140414130214)
- asana (0.0.6)
- activeresource (>= 3.2.3)
- asciidoctor (1.5.2)
+ arel (6.0.3)
+ asana (0.4.0)
+ faraday (~> 0.9)
+ faraday_middleware (~> 0.9)
+ faraday_middleware-multi_json (~> 0.0)
+ oauth2 (~> 1.0)
+ asciidoctor (1.5.3)
ast (2.1.0)
astrolabe (1.3.1)
parser (~> 2.2)
attr_encrypted (1.3.4)
encryptor (>= 1.3.0)
attr_required (1.0.0)
- autoprefixer-rails (5.2.1.2)
+ autoprefixer-rails (6.1.1)
execjs
json
awesome_print (1.2.0)
@@ -65,7 +73,9 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
+ babosa (1.0.2)
bcrypt (3.1.10)
+ benchmark-ips (2.3.0)
better_errors (1.0.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
@@ -84,9 +94,15 @@ GEM
ruby_parser (~> 3.5.0)
sass (~> 3.0)
terminal-table (~> 1.4)
- browser (1.0.0)
+ browser (1.0.1)
builder (3.2.2)
- byebug (6.0.2)
+ bullet (4.14.10)
+ activesupport (>= 3.0.0)
+ uniform_notifier (~> 1.9.0)
+ bundler-audit (0.4.0)
+ bundler (~> 1.2)
+ thor (~> 0.18)
+ byebug (8.2.0)
cal-heatmap-rails (0.0.1)
capybara (2.4.4)
mime-types (>= 1.16)
@@ -101,11 +117,25 @@ GEM
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
json (>= 1.7)
- celluloid (0.16.0)
- timers (~> 4.0.0)
- charlock_holmes (0.6.9.4)
- chronic (0.10.2)
- chunky_png (1.3.4)
+ celluloid (0.17.2)
+ celluloid-essentials
+ celluloid-extras
+ celluloid-fsm
+ celluloid-pool
+ celluloid-supervision
+ timers (>= 4.1.1)
+ celluloid-essentials (0.20.5)
+ timers (>= 4.1.1)
+ celluloid-extras (0.20.5)
+ timers (>= 4.1.1)
+ celluloid-fsm (0.20.5)
+ timers (>= 4.1.1)
+ celluloid-pool (0.20.5)
+ timers (>= 4.1.1)
+ celluloid-supervision (0.20.5)
+ timers (>= 4.1.1)
+ charlock_holmes (0.7.3)
+ chunky_png (1.3.5)
cliver (0.3.2)
coderay (1.1.0)
coercible (1.0.0)
@@ -116,24 +146,25 @@ GEM
coffee-script (2.4.1)
coffee-script-source
execjs
- coffee-script-source (1.9.1.1)
- colored (1.2)
- colorize (0.5.8)
+ coffee-script-source (1.10.0)
+ colorize (0.7.7)
connection_pool (2.2.0)
- coveralls (0.8.2)
+ coveralls (0.8.9)
json (~> 1.8)
rest-client (>= 1.6.8, < 2)
simplecov (~> 0.10.0)
term-ansicolor (~> 1.3)
thor (~> 0.19.1)
+ tins (~> 1.6.0)
crack (0.4.2)
safe_yaml (~> 1.0.0)
- creole (0.3.8)
+ creole (0.5.0)
d3_rails (3.5.6)
railties (>= 3.1.0)
daemons (1.2.3)
database_cleaner (1.4.1)
debug_inspector (0.0.2)
+ debugger-ruby_core_source (1.3.8)
default_value_for (3.0.1)
activerecord (>= 3.2.0, < 5.0)
descendants_tracker (0.0.4)
@@ -147,7 +178,7 @@ GEM
warden (~> 1.2.3)
devise-async (0.9.0)
devise (~> 3.2)
- devise-two-factor (2.0.0)
+ devise-two-factor (2.0.1)
activesupport
attr_encrypted (~> 1.3.2)
devise (~> 3.5.0)
@@ -156,22 +187,20 @@ GEM
diff-lcs (1.2.5)
diffy (3.0.7)
docile (1.1.5)
- domain_name (0.5.24)
+ domain_name (0.5.25)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (2.1.4)
+ doorkeeper (2.2.2)
railties (>= 3.2)
- dropzonejs-rails (0.7.1)
+ dropzonejs-rails (0.7.2)
rails (> 3.1)
email_reply_parser (0.5.8)
email_spec (1.6.0)
launchy (~> 2.1)
mail (~> 2.2)
encryptor (1.3.0)
- enumerize (0.7.0)
- activesupport (>= 3.2)
equalizer (0.0.11)
erubis (2.7.0)
- escape_utils (0.2.4)
+ escape_utils (1.1.0)
eventmachine (1.0.8)
excon (0.45.4)
execjs (2.6.0)
@@ -181,16 +210,25 @@ GEM
factory_girl_rails (4.3.0)
factory_girl (~> 4.3.0)
railties (>= 3.0.0)
- faraday (0.8.10)
- multipart-post (~> 1.2.0)
+ faraday (0.9.2)
+ multipart-post (>= 1.2, < 3)
faraday_middleware (0.10.0)
faraday (>= 0.7.4, < 0.10)
+ faraday_middleware-multi_json (0.0.6)
+ faraday_middleware
+ multi_json
fastercsv (1.5.5)
ffaker (2.0.0)
ffi (1.9.10)
fission (0.5.0)
CFPropertyList (~> 2.2)
- flowdock (0.7.0)
+ flay (2.6.1)
+ ruby_parser (~> 3.0)
+ sexp_processor (~> 4.0)
+ flog (4.3.2)
+ ruby_parser (~> 3.1, > 3.1.0)
+ sexp_processor (~> 4.4)
+ flowdock (0.7.1)
httparty (~> 0.7)
multi_json
fog (1.25.0)
@@ -212,13 +250,10 @@ GEM
fog-core (~> 1.22)
fog-json
inflecto (~> 0.0.2)
- fog-core (1.32.1)
+ fog-core (1.35.0)
builder
excon (~> 0.45)
formatador (~> 0.2)
- mime-types
- net-scp (~> 1.1)
- net-ssh (>= 2.1.3)
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
@@ -230,10 +265,10 @@ GEM
fog-core (>= 1.21.0)
fog-json
fog-xml (>= 0.0.1)
- fog-sakuracloud (1.0.1)
+ fog-sakuracloud (1.4.0)
fog-core
fog-json
- fog-softlayer (0.4.7)
+ fog-softlayer (1.0.2)
fog-core
fog-json
fog-terremark (0.1.0)
@@ -248,7 +283,7 @@ GEM
fog-xml (0.1.2)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
- font-awesome-rails (4.4.0.0)
+ font-awesome-rails (4.5.0.0)
railties (>= 3.2, < 5.0)
foreman (0.78.0)
thor (~> 0.19.1)
@@ -258,10 +293,15 @@ GEM
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
- gemojione (2.0.1)
+ gemojione (2.1.0)
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
+ github-linguist (4.7.2)
+ charlock_holmes (~> 0.7.3)
+ escape_utils (~> 1.1.0)
+ mime-types (>= 1.19)
+ rugged (>= 0.23.0b)
github-markup (1.3.3)
gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7)
@@ -272,23 +312,21 @@ GEM
diff-lcs (~> 1.1)
mime-types (~> 1.15)
posix-spawn (~> 0.3)
- gitlab-linguist (3.0.1)
- charlock_holmes (~> 0.6.6)
- escape_utils (~> 0.2.4)
- mime-types (~> 1.19)
- gitlab_emoji (0.1.1)
- gemojione (~> 2.0)
- gitlab_git (7.2.15)
+ gitlab_emoji (0.2.0)
+ gemojione (~> 2.1)
+ gitlab_git (7.2.20)
activesupport (~> 4.0)
- charlock_holmes (~> 0.6)
- gitlab-linguist (~> 3.0)
- rugged (~> 0.22.2)
+ charlock_holmes (~> 0.7.3)
+ github-linguist (~> 4.7.0)
+ rugged (~> 0.23.3)
gitlab_meta (7.0)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
pyu-ruby-sasl (~> 0.0.3.1)
rubyntlm (~> 0.3)
+ globalid (0.3.6)
+ activesupport (>= 4.1.0)
gollum-grit_adapter (1.0.0)
gitlab-grit (~> 2.7, >= 2.7.1)
gollum-lib (4.0.3)
@@ -298,13 +336,15 @@ GEM
rouge (~> 1.10.1)
sanitize (~> 2.1.0)
stringex (~> 2.5.1)
- gon (5.0.4)
- actionpack (>= 2.3.0)
+ gon (6.0.1)
+ actionpack (>= 3.0)
json
- grape (0.6.1)
+ multi_json
+ request_store (>= 1.0)
+ grape (0.13.0)
activesupport
builder
- hashie (>= 1.2.0)
+ hashie (>= 2.1.0)
multi_json (>= 1.3.2)
multi_xml (>= 0.5.2)
rack (>= 1.3.0)
@@ -314,27 +354,15 @@ GEM
grape-entity (0.4.8)
activesupport
multi_json (>= 1.3.2)
- growl (1.0.3)
- guard (2.13.0)
- formatador (>= 0.2.4)
- listen (>= 2.7, <= 4.0)
- lumberjack (~> 1.0)
- nenv (~> 0.1)
- notiffany (~> 0.0)
- pry (>= 0.9.12)
- shellany (~> 0.0)
- thor (>= 0.18.1)
- guard-rspec (4.2.10)
- guard (~> 2.1)
- rspec (>= 2.14, < 4.0)
haml (4.0.7)
tilt
- haml-rails (0.5.3)
+ haml-rails (0.9.0)
actionpack (>= 4.0.1)
activesupport (>= 4.0.1)
- haml (>= 3.1, < 5.0)
+ haml (>= 4.0.6, < 5.0)
+ html2haml (>= 1.0.1)
railties (>= 4.0.1)
- hashie (2.1.2)
+ hashie (3.4.3)
highline (1.6.21)
hike (1.2.3)
hipchat (1.5.2)
@@ -344,49 +372,54 @@ GEM
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.4)
+ html2haml (2.0.0)
+ erubis (~> 2.7.0)
+ haml (~> 4.0.0)
+ nokogiri (~> 1.6.0)
+ ruby_parser (~> 3.5)
http-cookie (1.0.2)
domain_name (~> 0.5)
http_parser.rb (0.5.3)
- httparty (0.13.5)
+ httparty (0.13.7)
json (~> 1.8)
multi_xml (>= 0.5.2)
- httpclient (2.6.0.1)
+ httpclient (2.7.0.1)
i18n (0.7.0)
- ice_cube (0.11.1)
ice_nine (0.11.1)
inflecto (0.0.2)
ipaddress (0.8.0)
- jquery-atwho-rails (1.0.1)
- jquery-rails (3.1.3)
+ jquery-atwho-rails (1.3.2)
+ jquery-rails (3.1.4)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
jquery-scrollto-rails (1.4.3)
railties (> 3.1, < 5.0)
- jquery-turbolinks (2.0.2)
+ jquery-turbolinks (2.1.0)
railties (>= 3.1.0)
turbolinks
jquery-ui-rails (4.2.1)
railties (>= 3.2.16)
json (1.8.3)
- jwt (1.5.1)
+ jwt (1.5.2)
kaminari (0.16.3)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
- kgio (2.9.3)
+ kgio (2.10.0)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.1.2)
launchy (~> 2.2)
- listen (2.10.1)
- celluloid (~> 0.16.0)
+ listen (2.9.0)
+ celluloid (>= 0.15.2)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
- lumberjack (1.0.9)
+ loofah (2.0.3)
+ nokogiri (>= 1.5.9)
macaddr (1.7.1)
systemu (~> 2.6.2)
mail (2.6.3)
mime-types (>= 1.16, < 3)
- mail_room (0.5.2)
+ mail_room (0.6.1)
method_source (0.8.2)
mime-types (1.25.1)
mimemagic (0.3.0)
@@ -395,25 +428,19 @@ GEM
mousetrap-rails (1.4.6)
multi_json (1.11.2)
multi_xml (0.5.5)
- multipart-post (1.2.0)
+ multipart-post (2.0.0)
mysql2 (0.3.20)
- nenv (0.2.0)
nested_form (0.3.2)
- net-ldap (0.11)
- net-scp (1.2.1)
- net-ssh (>= 2.6.5)
- net-ssh (2.9.2)
- netrc (0.10.3)
+ net-ldap (0.12.1)
+ net-ssh (3.0.1)
+ netrc (0.11.0)
newrelic-grape (2.0.0)
grape
newrelic_rpm
newrelic_rpm (3.9.4.245)
- nokogiri (1.6.6.2)
+ nokogiri (1.6.6.4)
mini_portile (~> 0.6.0)
- notiffany (0.0.7)
- nenv (~> 0.1)
- shellany (~> 0.0)
- nprogress-rails (0.1.2.3)
+ nprogress-rails (0.1.6.7)
oauth (0.4.7)
oauth2 (1.0.0)
faraday (>= 0.8, < 0.10)
@@ -430,16 +457,21 @@ GEM
multi_json (~> 1.7)
omniauth (~> 1.1)
omniauth-oauth (~> 1.0)
+ omniauth-facebook (3.0.0)
+ omniauth-oauth2 (~> 1.2)
omniauth-github (1.1.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
- omniauth-gitlab (1.0.0)
+ omniauth-gitlab (1.0.1)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
- omniauth-google-oauth2 (0.2.6)
- omniauth (> 1.0)
- omniauth-oauth2 (~> 1.1)
- omniauth-kerberos (0.2.0)
+ omniauth-google-oauth2 (0.2.10)
+ addressable (~> 2.3)
+ jwt (~> 1.0)
+ multi_json (~> 1.3)
+ omniauth (>= 1.1.1)
+ omniauth-oauth2 (~> 1.3.1)
+ omniauth-kerberos (0.3.0)
omniauth-multipassword
timfel-krb5-auth (~> 0.8)
omniauth-multipassword (0.4.2)
@@ -453,27 +485,27 @@ GEM
omniauth-saml (1.4.1)
omniauth (~> 1.1)
ruby-saml (~> 1.0.0)
- omniauth-shibboleth (1.1.2)
+ omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0)
- omniauth-twitter (1.0.1)
- multi_json (~> 1.3)
- omniauth-oauth (~> 1.0)
+ omniauth-twitter (1.2.1)
+ json (~> 1.3)
+ omniauth-oauth (~> 1.1)
omniauth_crowd (2.2.3)
activesupport
nokogiri (>= 1.4.4)
omniauth (~> 1.0)
- opennebula (4.12.1)
+ opennebula (4.14.2)
json
nokogiri
rbvmomi
org-ruby (0.9.12)
rubypants (~> 0.2)
orm_adapter (0.5.0)
- paranoia (2.1.3)
+ paranoia (2.1.4)
activerecord (~> 4.0)
- parser (2.2.2.6)
+ parser (2.2.3.0)
ast (>= 1.1, < 3.0)
- pg (0.18.2)
+ pg (0.18.4)
poltergeist (1.6.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
@@ -481,7 +513,7 @@ GEM
websocket-driver (>= 0.2.0)
posix-spawn (0.3.11)
powerpack (0.0.9)
- pry (0.10.1)
+ pry (0.10.3)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
@@ -490,17 +522,15 @@ GEM
pyu-ruby-sasl (0.0.3.3)
quiet_assets (1.0.3)
railties (>= 3.1, < 5.0)
- rack (1.5.5)
+ rack (1.6.4)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.3.0)
rack
- rack-cors (0.2.9)
- rack-mini-profiler (0.9.7)
- rack (>= 1.1.3)
+ rack-cors (0.4.0)
rack-mount (0.8.3)
rack (>= 1.0.0)
- rack-oauth2 (1.0.10)
+ rack-oauth2 (1.2.1)
activesupport (>= 2.3)
attr_required (>= 0.0.5)
httpclient (>= 2.4)
@@ -510,44 +540,53 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (4.1.12)
- actionmailer (= 4.1.12)
- actionpack (= 4.1.12)
- actionview (= 4.1.12)
- activemodel (= 4.1.12)
- activerecord (= 4.1.12)
- activesupport (= 4.1.12)
+ rails (4.2.4)
+ actionmailer (= 4.2.4)
+ actionpack (= 4.2.4)
+ actionview (= 4.2.4)
+ activejob (= 4.2.4)
+ activemodel (= 4.2.4)
+ activerecord (= 4.2.4)
+ activesupport (= 4.2.4)
bundler (>= 1.3.0, < 2.0)
- railties (= 4.1.12)
- sprockets-rails (~> 2.0)
- rails-observers (0.1.2)
- activemodel (~> 4.0)
- railties (4.1.12)
- actionpack (= 4.1.12)
- activesupport (= 4.1.12)
+ railties (= 4.2.4)
+ sprockets-rails
+ rails-deprecated_sanitizer (1.0.3)
+ activesupport (>= 4.2.0.alpha)
+ rails-dom-testing (1.0.7)
+ activesupport (>= 4.2.0.beta, < 5.0)
+ nokogiri (~> 1.6.0)
+ rails-deprecated_sanitizer (>= 1.0.1)
+ rails-html-sanitizer (1.0.2)
+ loofah (~> 2.0)
+ railties (4.2.4)
+ actionpack (= 4.2.4)
+ activesupport (= 4.2.4)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.0.0)
raindrops (0.15.0)
rake (10.4.2)
raphael-rails (2.1.2)
- rb-fsevent (0.9.5)
+ rb-fsevent (0.9.6)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
+ rblineprof (0.3.6)
+ debugger-ruby_core_source (~> 1.3)
rbvmomi (1.8.2)
builder
nokogiri (>= 1.4.1)
trollop
rdoc (3.12.2)
json (~> 1.4)
- redcarpet (3.3.2)
- redis (3.2.1)
- redis-actionpack (4.0.0)
+ redcarpet (3.3.3)
+ redis (3.2.2)
+ redis-actionpack (4.0.1)
actionpack (~> 4)
redis-rack (~> 1.5.0)
redis-store (~> 1.1.0)
- redis-activesupport (4.1.1)
- activesupport (~> 4)
+ redis-activesupport (4.1.5)
+ activesupport (>= 3, < 5)
redis-store (~> 1.1.0)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
@@ -558,13 +597,13 @@ GEM
redis-actionpack (~> 4)
redis-activesupport (~> 4)
redis-store (~> 1.1.0)
- redis-store (1.1.6)
+ redis-store (1.1.7)
redis (>= 2.2)
- request_store (1.2.0)
+ request_store (1.2.1)
rerun (0.10.0)
listen (~> 2.7, >= 2.7.3)
- responders (1.1.2)
- railties (>= 3.2, < 4.2)
+ responders (2.1.0)
+ railties (>= 4.2.0, < 5)
rest-client (1.8.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 3.0)
@@ -616,7 +655,8 @@ GEM
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
- rugged (0.22.2)
+ rufus-scheduler (3.1.10)
+ rugged (0.23.3)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
@@ -641,19 +681,17 @@ GEM
sexp_processor (4.6.0)
sham_rack (1.3.6)
rack
- shellany (0.0.1)
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
- sidekiq (3.3.0)
- celluloid (>= 0.16.0)
- connection_pool (>= 2.0.0)
- json
- redis (>= 3.0.6)
- redis-namespace (>= 1.3.1)
- sidetiq (0.6.3)
- celluloid (>= 0.14.1)
- ice_cube (= 0.11.1)
- sidekiq (>= 3.0.0)
+ sidekiq (3.5.3)
+ celluloid (~> 0.17.2)
+ connection_pool (~> 2.2, >= 2.2.0)
+ json (~> 1.0)
+ redis (~> 3.2, >= 3.2.1)
+ redis-namespace (~> 1.5, >= 1.5.2)
+ sidekiq-cron (0.3.1)
+ rufus-scheduler (>= 2.0.24)
+ sidekiq (>= 2.17.3)
simple_oauth (0.1.9)
simplecov (0.10.0)
docile (~> 1.1.0)
@@ -665,10 +703,7 @@ GEM
rack-protection (~> 1.4)
tilt (>= 1.3, < 3)
six (0.2.0)
- slack-notifier (1.0.0)
- slim (2.0.3)
- temple (~> 0.6.6)
- tilt (>= 1.3.3, < 2.1)
+ slack-notifier (1.2.1)
slop (3.6.0)
spinach (0.8.10)
colorize
@@ -690,12 +725,18 @@ GEM
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
- sprockets-rails (2.3.2)
+ sprockets-rails (2.3.3)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
- stamp (0.5.0)
- state_machine (1.2.0)
+ stamp (0.6.0)
+ state_machines (0.4.0)
+ state_machines-activemodel (0.3.0)
+ activemodel (~> 4.1)
+ state_machines (>= 0.4.0)
+ state_machines-activerecord (0.3.0)
+ activerecord (~> 4.1)
+ state_machines-activemodel (>= 0.3.0)
stringex (2.5.2)
systemu (2.6.5)
task_list (1.0.2)
@@ -704,29 +745,28 @@ GEM
railties (>= 3.2.5, < 5)
teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0)
- temple (0.6.10)
term-ansicolor (1.3.2)
tins (~> 1.0)
terminal-table (1.5.2)
- test_after_commit (0.2.7)
+ test_after_commit (0.4.2)
activerecord (>= 3.2)
- thin (1.6.3)
+ thin (1.6.4)
daemons (~> 1.0, >= 1.0.9)
- eventmachine (~> 1.0)
+ eventmachine (~> 1.0, >= 1.0.4)
rack (~> 1.0)
thor (0.19.1)
thread_safe (0.3.5)
tilt (1.4.1)
- timers (4.0.4)
+ timers (4.1.1)
hitimes
timfel-krb5-auth (0.8.3)
- tinder (1.9.4)
+ tinder (1.10.1)
eventmachine (~> 1.0)
- faraday (~> 0.8.9)
+ faraday (~> 0.9.0)
faraday_middleware (~> 0.9)
- hashie (>= 1.0, < 3)
+ hashie (>= 1.0)
json (~> 1.8.0)
- mime-types (~> 1.19)
+ mime-types
multi_json (~> 1.7)
twitter-stream (~> 0.1)
tins (1.6.0)
@@ -739,7 +779,7 @@ GEM
simple_oauth (~> 0.1.4)
tzinfo (1.2.2)
thread_safe (~> 0.1)
- uglifier (2.3.3)
+ uglifier (2.7.2)
execjs (>= 0.3.0)
json (>= 1.8.0)
underscore-rails (1.4.4)
@@ -750,9 +790,10 @@ GEM
kgio (~> 2.6)
rack
raindrops (~> 0.7)
- unicorn-worker-killer (0.4.3)
+ unicorn-worker-killer (0.4.4)
get_process_mem (~> 0)
- unicorn (~> 4)
+ unicorn (>= 4, < 6)
+ uniform_notifier (1.9.0)
uuid (2.3.8)
macaddr (~> 1.0)
version_sorter (2.0.0)
@@ -763,15 +804,17 @@ GEM
equalizer (~> 0.0, >= 0.0.9)
warden (1.2.3)
rack (>= 1.0)
+ web-console (2.2.1)
+ activemodel (>= 4.0)
+ binding_of_caller (>= 0.7.2)
+ railties (>= 4.0)
+ sprockets-rails (>= 2.0, < 4.0)
webmock (1.21.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
- websocket-driver (0.6.2)
+ websocket-driver (0.6.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
- whenever (0.8.4)
- activesupport (>= 2.3.4)
- chronic (>= 0.6.3)
wikicloth (0.8.1)
builder
expression_parser
@@ -791,26 +834,29 @@ DEPENDENCIES
addressable (~> 2.3.8)
after_commit_queue
annotate (~> 2.6.0)
- asana (~> 0.0.6)
+ asana (~> 0.4.0)
asciidoctor (~> 1.5.2)
attr_encrypted (~> 1.3.4)
awesome_print (~> 1.2.0)
+ babosa (~> 1.0.2)
+ benchmark-ips
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.0)
brakeman (= 3.0.1)
browser (~> 1.0.0)
+ bullet
+ bundler-audit
byebug
cal-heatmap-rails (~> 0.0.1)
capybara (~> 2.4.0)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.9.0)
- charlock_holmes (~> 0.6.9.4)
+ charlock_holmes (~> 0.7.3)
coffee-rails (~> 4.1.0)
- colored (~> 1.2)
- colorize (~> 0.5.8)
+ colorize (~> 0.7.0)
coveralls (~> 0.8.2)
- creole (~> 0.3.6)
+ creole (~> 0.5.0)
d3_rails (~> 3.5.5)
database_cleaner (~> 1.4.0)
default_value_for (~> 3.0.0)
@@ -818,61 +864,62 @@ DEPENDENCIES
devise-async (~> 0.9.0)
devise-two-factor (~> 2.0.0)
diffy (~> 3.0.3)
- doorkeeper (~> 2.1.3)
+ doorkeeper (~> 2.2.0)
dropzonejs-rails (~> 0.7.1)
email_reply_parser (~> 0.5.8)
email_spec (~> 1.6.0)
- enumerize (~> 0.7.0)
factory_girl_rails (~> 4.3.0)
ffaker (~> 2.0.0)
+ flay
+ flog
fog (~> 1.25.0)
font-awesome-rails (~> 4.2)
foreman
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
+ github-linguist (~> 4.7.0)
github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab-linguist (~> 3.0.1)
gitlab_emoji (~> 0.1)
- gitlab_git (~> 7.2.15)
+ gitlab_git (~> 7.2.20)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.0.2)
- gon (~> 5.0.0)
- grape (~> 0.6.1)
+ gon (~> 6.0.1)
+ grape (~> 0.13.0)
grape-entity (~> 0.4.2)
- growl
- guard-rspec (~> 4.2.0)
- haml-rails (~> 0.5.3)
+ haml-rails (~> 0.9.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
- jquery-atwho-rails (~> 1.0.0)
+ jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 3.1.3)
jquery-scrollto-rails (~> 1.4.3)
- jquery-turbolinks (~> 2.0.1)
+ jquery-turbolinks (~> 2.1.0)
jquery-ui-rails (~> 4.2.1)
kaminari (~> 0.16.3)
letter_opener (~> 1.1.2)
- mail_room (~> 0.5.2)
+ mail_room (~> 0.6.1)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16)
nested_form (~> 0.3.2)
+ net-ssh (~> 3.0.1)
newrelic-grape
newrelic_rpm (~> 3.9.4.245)
- nprogress-rails (~> 0.1.2.3)
+ nprogress-rails (~> 0.1.6.7)
oauth2 (~> 1.0.0)
octokit (~> 3.7.0)
omniauth (~> 1.2.2)
omniauth-bitbucket (~> 0.0.2)
+ omniauth-facebook (~> 3.0.0)
omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.0)
- omniauth-google-oauth2 (~> 0.2.5)
- omniauth-kerberos (~> 0.2.0)
+ omniauth-google-oauth2 (~> 0.2.0)
+ omniauth-kerberos (~> 0.3.0)
omniauth-saml (~> 1.4.0)
- omniauth-shibboleth (~> 1.1.1)
- omniauth-twitter (~> 1.0.1)
+ omniauth-shibboleth (~> 1.2.0)
+ omniauth-twitter (~> 1.2.0)
omniauth_crowd
org-ruby (~> 0.9.12)
paranoia (~> 2.0)
@@ -881,18 +928,19 @@ DEPENDENCIES
pry-rails
quiet_assets (~> 1.0.2)
rack-attack (~> 4.3.0)
- rack-cors (~> 0.2.9)
- rack-mini-profiler (~> 0.9.0)
- rack-oauth2 (~> 1.0.5)
- rails (= 4.1.12)
+ rack-cors (~> 0.4.0)
+ rack-oauth2 (~> 1.2.1)
+ rails (= 4.2.4)
+ rails-deprecated_sanitizer (~> 1.0.3)
raphael-rails (~> 2.1.2)
- rb-fsevent
- rb-inotify
+ rblineprof
rdoc (~> 3.6)
- redcarpet (~> 3.3.2)
+ redcarpet (~> 3.3.3)
redis-rails (~> 4.0.0)
request_store (~> 1.2.0)
rerun (~> 0.10.0)
+ responders (~> 2.0)
+ rouge (~> 1.10.1)
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0)
rubocop (~> 0.28.0)
@@ -905,35 +953,37 @@ DEPENDENCIES
settingslogic (~> 2.0.9)
sham_rack
shoulda-matchers (~> 2.8.0)
- sidekiq (= 3.3.0)
- sidetiq (~> 0.6.3)
+ sidekiq (~> 3.5.0)
+ sidekiq-cron (~> 0.3.0)
simplecov (~> 0.10.0)
sinatra (~> 1.4.4)
six (~> 0.2.0)
- slack-notifier (~> 1.0.0)
- slim (~> 2.0.2)
+ slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1)
spring (~> 1.3.6)
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.0.0)
spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 2.12.3)
- stamp (~> 0.5.0)
- state_machine (~> 1.2.0)
+ stamp (~> 0.6.0)
+ state_machines-activerecord (~> 0.3.0)
task_list (~> 1.0.2)
teaspoon (~> 1.0.0)
teaspoon-jasmine (~> 2.2.0)
- test_after_commit (~> 0.2.2)
+ test_after_commit (~> 0.4.2)
thin (~> 1.6.1)
- tinder (~> 1.9.2)
+ tinder (~> 1.10.0)
turbolinks (~> 2.5.0)
- uglifier (~> 2.3.2)
+ uglifier (~> 2.7.2)
underscore-rails (~> 1.4.4)
unf (~> 0.1.4)
unicorn (~> 4.8.2)
unicorn-worker-killer (~> 0.4.2)
version_sorter (~> 2.0.0)
virtus (~> 1.0.1)
+ web-console (~> 2.0)
webmock (~> 1.21.0)
- whenever (~> 0.8.4)
wikicloth (= 0.8.1)
+
+BUNDLED WITH
+ 1.10.6
diff --git a/PROCESS.md b/PROCESS.md
index 1b6b3e7d32d..72fc3481447 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -34,13 +34,19 @@ The most important thing is making sure valid issues receive feedback from the d
## Workflow labels
-Workflow labels are purposely not very detailed since that would be hard to keep updated as you would need to re-evaluate them after every comment. We optionally use functional labels on demand when want to group related issues to get an overview (for example all issues related to RVM, to tackle them in one go) and to add details to the issue.
+Workflow labels are purposely not very detailed since that would be hard to keep updated as you would need to re-evaluate them after every comment. We optionally use functional labels on demand when want to group related issues to get an overview (for example all issues related to RVM, to tackle them in one go) and to add details to the issue.
- *Awaiting feedback*: Feedback pending from the reporter
- *Awaiting confirmation of fix*: The issue should already be solved in **master** (generally you can avoid this workflow item and just close the issue right away)
- *Attached MR*: There is a MR attached and the discussion should happen there
- We need to let issues stay in sync with the MR's. We can do this with a "Closing #XXXX" or "Fixes #XXXX" comment in the MR. We can't close the issue when there is a merge request because sometimes a MR is not good and we just close the MR, then the issue must stay.
-- *Awaiting developer action/feedback*: Issue needs to be fixed or clarified by a developer
+- *Developer*: needs help from a developer
+- *UX* needs needs help from a UX designer
+- *Frontend* needs help from a Front-end engineer
+- *Graphics* needs help from a Graphics designer
+- *up-for-grabs* is an issue suitable for first-time contributors, of reasonable difficulty and size. Not exclusive with other labels.
+
+Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label.
## Functional labels
@@ -79,7 +85,11 @@ Thanks for the issue report but we only support issues for the latest stable ver
### Support requests and configuration questions
-Thanks for your interest in GitLab. We don't use the issue tracker for support requests and configuration questions. Please use the \[support forum\]\(https://groups.google.com/forum/#!forum/gitlabhq), \[Stack Overflow\]\(http://stackoverflow.com/questions/tagged/gitlab), the #gitlab IRC channel on Freenode or the http://about.gitlab.com paid services for this purpose. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information.
+Thanks for your interest in GitLab. We don't use the issue tracker for support
+requests and configuration questions. Please check our
+\[getting help\]\(https://about.gitlab.com/getting-help/) page to see all of the available
+support options. Also, have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md)
+for more information.
### Code format
@@ -111,3 +121,10 @@ We can only accept a merge request if all the tests are green. I've just
restarted the build. When the tests are still not passing after this restart and
you're sure that is does not have anything to do with your code changes, please
rebase with master to see if that solves the issue.
+
+### Closing down the issue tracker on GitHub
+
+We are currently in the process of closing down the issue tracker on GitHub, to
+prevent duplication with the GitLab.com issue tracker.
+Since this is an older issue I'll be closing this for now. If you think this is
+still an issue I encourage you to open it on the \[GitLab.com issue tracker\](https://gitlab.com/gitlab-org/gitlab-ce/issues).
diff --git a/Procfile b/Procfile
index 08880b9c425..9cfdee7040f 100644
--- a/Procfile
+++ b/Procfile
@@ -1,3 +1,7 @@
+# For DEVELOPMENT only. Production uses Runit in
+# https://gitlab.com/gitlab-org/omnibus-gitlab or the init scripts in
+# lib/support/init.d, which call scripts in bin/ .
+#
web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"}
-worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default
+worker: bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default
# mail_room: bundle exec mail_room -q -c config/mail_room.yml
diff --git a/README.md b/README.md
index 99d5bc0b6ca..c459e67baa1 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# GitLab
-[![build status](https://ci.gitlab.com/projects/1/status.png?ref=master)](https://ci.gitlab.com/projects/1?ref=master)
+[![build status](https://ci.gitlab.com/projects/1/status.svg?ref=master)](https://ci.gitlab.com/projects/1?ref=master)
[![Build Status](https://semaphoreci.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/400484/shields_badge.svg)](https://semaphoreci.com/gitlabhq/gitlabhq)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.svg?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master)
@@ -27,8 +27,6 @@ There are two editions of GitLab:
- GitLab Community Edition (CE) is available freely under the MIT Expat license.
- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/features/#compare) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/).
-Included with the GitLab Omnibus Packages is [GitLab CI](https://about.gitlab.com/gitlab-ci/) that can easily build, test and deploy code.
-
## Website
On [about.gitlab.com](https://about.gitlab.com/) you can find more information about:
@@ -71,7 +69,7 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL
- Ruby (MRI) 2.1
- Git 1.7.10+
-- Redis 2.0+
+- Redis 2.4+
- MySQL or PostgreSQL
For more information please see the [architecture documentation](http://doc.gitlab.com/ce/development/architecture.html).
@@ -82,7 +80,7 @@ There are a lot of [third-party applications integrating with GitLab](https://ab
## GitLab release cycle
-Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). For more information about the release process see the [release documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/release). Features that will likely be in the next releases can be found on the [feature request forum](http://feedback.gitlab.com/forums/176466-general) with the status [started](http://feedback.gitlab.com/forums/176466-general/status/796456) and [completed](http://feedback.gitlab.com/forums/176466-general/status/796457).
+For more information about the release process see the [release documentation](http://doc.gitlab.com/ce/release/).
## Upgrading
diff --git a/VERSION b/VERSION
index a2264f05f50..8d0676ff07b 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.1.0.pre
+8.3.0.pre
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf b/app/assets/fonts/SourceSansPro-Black.ttf
index cb89a2d171e..9c9b5cb7f03 100755..100644
--- a/app/assets/fonts/SourceSansPro-Black.ttf
+++ b/app/assets/fonts/SourceSansPro-Black.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf b/app/assets/fonts/SourceSansPro-BlackIt.ttf
new file mode 100644
index 00000000000..294ce5abe8f
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BlackIt.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf b/app/assets/fonts/SourceSansPro-Bold.ttf
index 50d81bdad58..5d65c93242f 100755..100644
--- a/app/assets/fonts/SourceSansPro-Bold.ttf
+++ b/app/assets/fonts/SourceSansPro-Bold.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf b/app/assets/fonts/SourceSansPro-BoldIt.ttf
new file mode 100644
index 00000000000..3decd130070
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BoldIt.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf b/app/assets/fonts/SourceSansPro-ExtraLight.ttf
index bb4176c6fff..253eafa3783 100755..100644
--- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf
+++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf
new file mode 100644
index 00000000000..00d7e9a7aa8
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf b/app/assets/fonts/SourceSansPro-It.ttf
new file mode 100644
index 00000000000..f7af5377595
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-It.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf b/app/assets/fonts/SourceSansPro-Light.ttf
index 5f64679f6b9..83a0a336661 100755..100644
--- a/app/assets/fonts/SourceSansPro-Light.ttf
+++ b/app/assets/fonts/SourceSansPro-Light.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf b/app/assets/fonts/SourceSansPro-LightIt.ttf
new file mode 100644
index 00000000000..f18827985ef
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-LightIt.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf b/app/assets/fonts/SourceSansPro-Regular.ttf
index 91e9ea5757f..44486cdc670 100755..100644
--- a/app/assets/fonts/SourceSansPro-Regular.ttf
+++ b/app/assets/fonts/SourceSansPro-Regular.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf b/app/assets/fonts/SourceSansPro-Semibold.ttf
index 5020594826b..86b00c067e0 100755..100644
--- a/app/assets/fonts/SourceSansPro-Semibold.ttf
+++ b/app/assets/fonts/SourceSansPro-Semibold.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf
new file mode 100644
index 00000000000..13d66a1fc45
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf
Binary files differ
diff --git a/app/assets/images/auth_buttons/facebook_64.png b/app/assets/images/auth_buttons/facebook_64.png
new file mode 100644
index 00000000000..1f1a80d7368
--- /dev/null
+++ b/app/assets/images/auth_buttons/facebook_64.png
Binary files differ
diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png
index 60021d5ac47..7d89da97e11 100644
--- a/app/assets/images/icon-link.png
+++ b/app/assets/images/icon-link.png
Binary files differ
diff --git a/app/assets/images/logo.svg b/app/assets/images/logo.svg
index c09785cb96f..f4e19b67008 100644
--- a/app/assets/images/logo.svg
+++ b/app/assets/images/logo.svg
@@ -10,17 +10,17 @@
<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"></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"></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"></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"></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"></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"></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"></path>
+ <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> \ No newline at end of file
+</svg>
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 8e987ac4e83..1539eba0faa 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -135,17 +135,25 @@ $ ->
), 1
# Initialize tooltips
- $('body').tooltip({
- selector: '.has_tooltip, [data-toggle="tooltip"], .page-sidebar-collapsed .nav-sidebar a'
+ $('body').tooltip(
+ selector: '.has_tooltip, [data-toggle="tooltip"]'
placement: (_, el) ->
$el = $(el)
- if $el.attr('id') == 'js-shortcuts-home'
- # Place the logo tooltip on the right when collapsed, bottom when expanded
- $el.parents('header').hasClass('header-collapsed') and 'right' or 'bottom'
- else
- # Otherwise use the data-placement attribute, or 'bottom' if undefined
- $el.data('placement') or 'bottom'
- })
+ $el.data('placement') || 'bottom'
+ )
+
+ $('.header-logo .home').tooltip(
+ placement: (_, el) ->
+ $el = $(el)
+ if $('.page-with-sidebar').hasClass('page-sidebar-collapsed') then 'right' else 'bottom'
+ container: 'body'
+ )
+
+ $('.page-with-sidebar').tooltip(
+ selector: '.sidebar-collapsed .nav-sidebar a, .sidebar-collapsed a.sidebar-user'
+ placement: 'right'
+ container: 'body'
+ )
# Form submitter
$('.trigger-submit').on 'change', ->
@@ -180,6 +188,7 @@ $ ->
$('.navbar-toggle').on 'click', ->
$('.header-content .title').toggle()
$('.header-content .navbar-collapse').toggle()
+ $('.navbar-toggle').toggleClass('active')
# Show/hide comments on diff
$("body").on "click", ".js-toggle-diff-comments", (e) ->
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
new file mode 100644
index 00000000000..96fd8f8773e
--- /dev/null
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -0,0 +1,96 @@
+class @AwardsHandler
+ constructor: (@post_emoji_url, @noteable_type, @noteable_id) ->
+
+ addAward: (emoji) ->
+ @postEmoji emoji, =>
+ @addAwardToEmojiBar(emoji)
+
+ addAwardToEmojiBar: (emoji, custom_path = '') ->
+ if @exist(emoji)
+ if @isActive(emoji)
+ @decrementCounter(emoji)
+ else
+ counter = @findEmojiIcon(emoji).siblings(".counter")
+ counter.text(parseInt(counter.text()) + 1)
+ counter.parent().addClass("active")
+ @addMeToAuthorList(emoji)
+ else
+ @createEmoji(emoji, custom_path)
+
+ exist: (emoji) ->
+ @findEmojiIcon(emoji).length > 0
+
+ isActive: (emoji) ->
+ @findEmojiIcon(emoji).parent().hasClass("active")
+
+ decrementCounter: (emoji) ->
+ counter = @findEmojiIcon(emoji).siblings(".counter")
+
+ if parseInt(counter.text()) > 1
+ counter.text(parseInt(counter.text()) - 1)
+ counter.parent().removeClass("active")
+ @removeMeFromAuthorList(emoji)
+ else
+ award = counter.parent()
+ award.tooltip("destroy")
+ award.remove()
+
+ removeMeFromAuthorList: (emoji) ->
+ award_block = @findEmojiIcon(emoji).parent()
+ authors = award_block.attr("data-original-title").split(", ")
+ authors = _.without(authors, "me").join(", ")
+ award_block.attr("title", authors)
+ @resetTooltip(award_block)
+
+ addMeToAuthorList: (emoji) ->
+ award_block = @findEmojiIcon(emoji).parent()
+ authors = award_block.attr("data-original-title").split(", ")
+ authors.push("me")
+ award_block.attr("title", authors.join(", "))
+ @resetTooltip(award_block)
+
+ resetTooltip: (award) ->
+ award.tooltip("destroy")
+
+ # "destroy" call is asynchronous, this is why we need to set timeout.
+ setTimeout (->
+ award.tooltip()
+ ), 200
+
+
+ createEmoji: (emoji, custom_path) ->
+ nodes = []
+ nodes.push("<div class='award active' title='me'>")
+ nodes.push("<div class='icon' data-emoji='" + emoji + "'>")
+ nodes.push(@getImage(emoji, custom_path))
+ nodes.push("</div>")
+ nodes.push("<div class='counter'>1")
+ nodes.push("</div></div>")
+
+ $(".awards-controls").before(nodes.join("\n"))
+
+ $(".award").tooltip()
+
+ getImage: (emoji, custom_path) ->
+ if custom_path
+ $("<img>").attr({src: custom_path, width: 20, height: 20}).wrap("<div>").parent().html()
+ else
+ $("li[data-emoji='" + emoji + "']").html()
+
+
+ postEmoji: (emoji, callback) ->
+ $.post @post_emoji_url, { note: {
+ note: ":" + emoji + ":"
+ noteable_type: @noteable_type
+ noteable_id: @noteable_id
+ }},(data) ->
+ if data.ok
+ callback.call()
+
+ findEmojiIcon: (emoji) ->
+ $(".icon[data-emoji='" + emoji + "']")
+
+ scrollToAwards: ->
+ $('body, html').animate({
+ scrollTop: $('.awards').offset().top - 80
+ }, 200)
diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee
new file mode 100644
index 00000000000..4ec8531d580
--- /dev/null
+++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee
@@ -0,0 +1,29 @@
+# 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.
+#
+#= require extensions/jquery
+#
+# ### Example Markup
+#
+# <form action="/foo">
+# <input type="text" class="js-quick-submit" />
+# <textarea class="js-quick-submit"></textarea>
+# </form>
+#
+$(document).on 'keydown.quick_submit', '.js-quick-submit', (e) ->
+ return if (e.originalEvent && e.originalEvent.repeat) || e.repeat
+ return unless e.keyCode == 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)
+
+ e.preventDefault()
+
+ $form = $(e.target).closest('form')
+ $form.find('input[type=submit], button[type=submit]').disable()
+ $form.submit()
diff --git a/app/assets/javascripts/behaviors/requires_input.js.coffee b/app/assets/javascripts/behaviors/requires_input.js.coffee
index 8318fe435b3..79d750d1847 100644
--- a/app/assets/javascripts/behaviors/requires_input.js.coffee
+++ b/app/assets/javascripts/behaviors/requires_input.js.coffee
@@ -34,6 +34,5 @@ $.fn.requiresInput = ->
$form.on 'change input', fieldSelector, requireInput
-# Triggered on standard document `ready` and on Turbolinks `page:load` events
-$(document).on 'ready page:load', ->
+$ ->
$('form.js-requires-input').requiresInput()
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee
index 3ab3ba66754..195f8b11e5d 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee
@@ -23,18 +23,6 @@ class @BlobFileDropzone
init: ->
this.on 'addedfile', (file) ->
$('.dropzone-alerts').html('').hide()
- commit_message = form.find('#commit_message')[0]
-
- if /^Upload/.test(commit_message.placeholder)
- commit_message.placeholder = 'Upload ' + file.name
-
- return
-
- this.on 'removedfile', (file) ->
- commit_message = form.find('#commit_message')[0]
-
- if /^Upload/.test(commit_message.placeholder)
- commit_message.placeholder = 'Upload new file'
return
@@ -47,7 +35,9 @@ class @BlobFileDropzone
return
this.on 'sending', (file, xhr, formData) ->
- formData.append('commit_message', form.find('#commit_message').val())
+ formData.append('new_branch', form.find('.js-new-branch').val())
+ formData.append('create_merge_request', form.find('.js-create-merge-request').val())
+ formData.append('commit_message', form.find('.js-commit-message').val())
return
# Override behavior of adding error underneath preview
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
index 050888f9c15..f6bf836f19f 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -11,10 +11,10 @@ class @EditBlob
if ace_mode
editor.getSession().setMode "ace/mode/" + ace_mode
- $(".js-commit-button").click ->
- $("#file-content").val editor.getValue()
- $(".file-editor form").submit()
- return false
+ # Before a form submission, move the content from the Ace editor into the
+ # submitted textarea
+ $('form').submit ->
+ $("#file-content").val(editor.getValue())
editModePanes = $(".js-edit-mode-pane")
editModeLinks = $(".js-edit-mode a")
diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee
index 1f36a53f191..68c5e5195e3 100644
--- a/app/assets/javascripts/blob/new_blob.js.coffee
+++ b/app/assets/javascripts/blob/new_blob.js.coffee
@@ -11,10 +11,10 @@ class @NewBlob
if ace_mode
editor.getSession().setMode "ace/mode/" + ace_mode
- $(".js-commit-button").click ->
- $("#file-content").val editor.getValue()
- $(".file-editor form").submit()
- return false
+ # Before a form submission, move the content from the Ace editor into the
+ # submitted textarea
+ $('form').submit ->
+ $("#file-content").val(editor.getValue())
editor: ->
return @editor
diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee
index 4c4bc3d66ed..97621236924 100644
--- a/app/assets/javascripts/calendar.js.coffee
+++ b/app/assets/javascripts/calendar.js.coffee
@@ -25,7 +25,7 @@ class @Calendar
30
]
legendCellPadding: 3
- cellSize: $('.user-calendar').width() / 80
+ cellSize: $('.user-calendar').width() / 73
onClick: (date, count) ->
formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate()
$.ajax
diff --git a/app/assets/javascripts/ci/Chart.min.js b/app/assets/javascripts/ci/Chart.min.js
deleted file mode 100644
index ab635881087..00000000000
--- a/app/assets/javascripts/ci/Chart.min.js
+++ /dev/null
@@ -1,39 +0,0 @@
-var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a=
-Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);d<b||d>c;)a=d<b?a/2:2*a,d=Math.round(e/a);c=[];z(f,c,d,h,a);return{steps:d,stepValue:a,graphMin:h,labels:c}}function z(a,c,b,e,h){if(a)for(var f=1;f<b+1;f++)c.push(E(a,{value:(e+h*f).toFixed(0!=h%1?h.toString().split(".")[1].length:0)}))}function A(a,c,b){return!isNaN(parseFloat(c))&&isFinite(c)&&a>c?c:!isNaN(parseFloat(b))&&
-isFinite(b)&&a<b?b:a}function y(a,c){var b={},e;for(e in a)b[e]=a[e];for(e in c)b[e]=c[e];return b}function E(a,c){var b=!/\W/.test(a)?F[a]=F[a]||E(document.getElementById(a).innerHTML):new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+a.replace(/[\r\t\n]/g," ").split("<%").join("\t").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c?
-b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)?
-0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1==
-a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*
-Math.PI)*Math.asin(1/e);return-(e*Math.pow(2,10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b))},easeOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return e*Math.pow(2,-10*a)*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(2==(a/=0.5))return 1;b||(b=1*0.3*1.5);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return 1>a?-0.5*e*Math.pow(2,10*
-(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)*
-a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0,
-scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",
-animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",
-scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a,
-c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,
-onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0,
-pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",
-scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]);
-d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.length;f++)a[f].value>e&&(e=a[f].value),a[f].value<h&&(h=a[f].value);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,
-m);k=g/j.steps;x(c,function(){for(var a=0;a<j.steps;a++)if(c.scaleShowLine&&(b.beginPath(),b.arc(q/2,u/2,k*(a+1),0,2*Math.PI,!0),b.strokeStyle=c.scaleLineColor,b.lineWidth=c.scaleLineWidth,b.stroke()),c.scaleShowLabels){b.textAlign="center";b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;var e=j.labels[a];if(c.scaleShowLabelBackdrop){var d=b.measureText(e).width;b.fillStyle=c.scaleBackdropColor;b.beginPath();b.rect(Math.round(q/2-d/2-c.scaleBackdropPaddingX),Math.round(u/2-k*(a+
-1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(d+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY));b.fill()}b.textBaseline="middle";b.fillStyle=c.scaleFontColor;b.fillText(e,q/2,u/2-k*(a+1))}},function(e){var d=-Math.PI/2,g=2*Math.PI/a.length,f=1,h=1;c.animation&&(c.animateScale&&(f=e),c.animateRotate&&(h=e));for(e=0;e<a.length;e++)b.beginPath(),b.arc(q/2,u/2,f*v(a[e].value,j,k),d,d+h*g,!1),b.lineTo(q/2,u/2),b.closePath(),b.fillStyle=a[e].color,b.fill(),
-c.segmentShowStroke&&(b.strokeStyle=c.segmentStrokeColor,b.lineWidth=c.segmentStrokeWidth,b.stroke()),d+=h*g},b)},H=function(a,c,b){var e,h,f,d,g,k,j,l,m;a.labels||(a.labels=[]);g=Math.min.apply(Math,[q,u])/2;d=2*c.scaleFontSize;for(e=l=0;e<a.labels.length;e++)b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily,h=b.measureText(a.labels[e]).width,h>l&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE;
-h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(m=0;m<a.datasets[f].data.length;m++)a.datasets[f].data[m]>e&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]<h&&(h=a.datasets[f].data[m]);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,m);k=g/j.steps;x(c,function(){var e=2*Math.PI/
-a.datasets[0].data.length;b.save();b.translate(q/2,u/2);if(c.angleShowLineOut){b.strokeStyle=c.angleLineColor;b.lineWidth=c.angleLineWidth;for(var d=0;d<a.datasets[0].data.length;d++)b.rotate(e),b.beginPath(),b.moveTo(0,0),b.lineTo(0,-g),b.stroke()}for(d=0;d<j.steps;d++){b.beginPath();if(c.scaleShowLine){b.strokeStyle=c.scaleLineColor;b.lineWidth=c.scaleLineWidth;b.moveTo(0,-k*(d+1));for(var f=0;f<a.datasets[0].data.length;f++)b.rotate(e),b.lineTo(0,-k*(d+1));b.closePath();b.stroke()}c.scaleShowLabels&&
-(b.textAlign="center",b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily,b.textBaseline="middle",c.scaleShowLabelBackdrop&&(f=b.measureText(j.labels[d]).width,b.fillStyle=c.scaleBackdropColor,b.beginPath(),b.rect(Math.round(-f/2-c.scaleBackdropPaddingX),Math.round(-k*(d+1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(f+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY)),b.fill()),b.fillStyle=c.scaleFontColor,b.fillText(j.labels[d],0,-k*(d+
-1)))}for(d=0;d<a.labels.length;d++){b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily;b.fillStyle=c.pointLabelFontColor;var f=Math.sin(e*d)*(g+c.pointLabelFontSize),h=Math.cos(e*d)*(g+c.pointLabelFontSize);b.textAlign=e*d==Math.PI||0==e*d?"center":e*d>Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;g<a.datasets.length;g++){b.beginPath();
-b.moveTo(0,d*-1*v(a.datasets[g].data[0],j,k));for(var f=1;f<a.datasets[g].data.length;f++)b.rotate(e),b.lineTo(0,d*-1*v(a.datasets[g].data[f],j,k));b.closePath();b.fillStyle=a.datasets[g].fillColor;b.strokeStyle=a.datasets[g].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.fill();b.stroke();if(c.pointDot){b.fillStyle=a.datasets[g].pointColor;b.strokeStyle=a.datasets[g].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(f=0;f<a.datasets[g].data.length;f++)b.rotate(e),b.beginPath(),b.arc(0,d*-1*
-v(a.datasets[g].data[f],j,k),c.pointDotRadius,2*Math.PI,!1),b.fill(),b.stroke()}b.rotate(e)}b.restore()},b)},I=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=0;f<a.length;f++)e+=a[f].value;x(c,null,function(d){var g=-Math.PI/2,f=1,j=1;c.animation&&(c.animateScale&&(f=d),c.animateRotate&&(j=d));for(d=0;d<a.length;d++){var l=j*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,f*h,g,g+l);b.lineTo(q/2,u/2);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&(b.lineWidth=
-c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());g+=l}},b)},J=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=h*(c.percentageInnerCutout/100),d=0;d<a.length;d++)e+=a[d].value;x(c,null,function(d){var k=-Math.PI/2,j=1,l=1;c.animation&&(c.animateScale&&(j=d),c.animateRotate&&(l=d));for(d=0;d<a.length;d++){var m=l*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,j*h,k,k+m,!1);b.arc(q/2,u/2,j*f,k+m,k,!0);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&
-(b.lineWidth=c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());k+=m}},b)},K=function(a,c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(s=45,q/a.labels.length<Math.cos(s)*t?(s=90,g-=t):g-=Math.sin(s)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=
-0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;
-for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<s?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<s?(b.translate(n+d*m,p+c.scaleFontSize),b.rotate(-(s*(Math.PI/180))),b.fillText(a.labels[d],
-0,0),b.restore()):b.fillText(a.labels[d],n+d*m,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+d*m,p+3),c.scaleShowGridLines&&0<d?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+d*m,5)):b.lineTo(n+d*m,p+3),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,
-b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){function e(b,c){return p-d*v(a.datasets[b].data[c],j,k)}for(var f=0;f<a.datasets.length;f++){b.strokeStyle=a.datasets[f].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.beginPath();b.moveTo(n,p-d*v(a.datasets[f].data[0],j,k));for(var g=1;g<a.datasets[f].data.length;g++)c.bezierCurve?b.bezierCurveTo(n+m*(g-0.5),e(f,g-1),n+m*(g-0.5),
-e(f,g),n+m*g,e(f,g)):b.lineTo(n+m*g,e(f,g));b.stroke();c.datasetFill?(b.lineTo(n+m*(a.datasets[f].data.length-1),p),b.lineTo(n,p),b.closePath(),b.fillStyle=a.datasets[f].fillColor,b.fill()):b.closePath();if(c.pointDot){b.fillStyle=a.datasets[f].pointColor;b.strokeStyle=a.datasets[f].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(g=0;g<a.datasets[f].data.length;g++)b.beginPath(),b.arc(n+m*g,p-d*v(a.datasets[f].data[g],j,k),c.pointDotRadius,0,2*Math.PI,!0),b.fill(),b.stroke()}}},b)},L=function(a,
-c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s,w=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(w=45,q/a.labels.length<Math.cos(w)*t?(w=90,g-=t):g-=Math.sin(w)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<
-h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=
-Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<w?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<w?(b.translate(n+
-d*m,p+c.scaleFontSize),b.rotate(-(w*(Math.PI/180))),b.fillText(a.labels[d],0,0),b.restore()):b.fillText(a.labels[d],n+d*m+m/2,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+(d+1)*m,p+3),b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+(d+1)*m,5),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*
-k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){b.lineWidth=c.barStrokeWidth;for(var e=0;e<a.datasets.length;e++){b.fillStyle=a.datasets[e].fillColor;b.strokeStyle=a.datasets[e].strokeColor;for(var f=0;f<a.datasets[e].data.length;f++){var g=n+c.barValueSpacing+m*f+s*e+c.barDatasetSpacing*e+c.barStrokeWidth*e;b.beginPath();
-b.moveTo(g,p);b.lineTo(g,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p);c.barShowStroke&&b.stroke();b.closePath();b.fill()}}},b)},D=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1E3/60)},F={}}; \ No newline at end of file
diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
index c30859b484b..44d5ddb7d95 100644
--- a/app/assets/javascripts/ci/build.coffee
+++ b/app/assets/javascripts/ci/build.coffee
@@ -22,7 +22,7 @@ class CiBuild
# Only valid for runnig build when output changes during time
#
CiBuild.interval = setInterval =>
- if window.location.href is build_url
+ if window.location.href.split("#").first() is build_url
$.ajax
url: build_url
dataType: "json"
@@ -31,7 +31,7 @@ class CiBuild
$('#build-trace code').html build.trace_html
$('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>'
@checkAutoscroll()
- else
+ else if build.status != build_status
Turbolinks.visit build_url
, 4000
diff --git a/app/assets/javascripts/ci/projects.js.coffee b/app/assets/javascripts/ci/projects.js.coffee
index 7e028b4e115..e6406011d11 100644
--- a/app/assets/javascripts/ci/projects.js.coffee
+++ b/app/assets/javascripts/ci/projects.js.coffee
@@ -1,6 +1,3 @@
$(document).on 'click', '.badge-codes-toggle', ->
$('.badge-codes-block').toggleClass("hide")
return false
-
-$(document).on 'click', '.sync-now', ->
- $(this).find('i').addClass('fa-spin')
diff --git a/app/assets/javascripts/copy_to_clipboard.js.coffee b/app/assets/javascripts/copy_to_clipboard.js.coffee
new file mode 100644
index 00000000000..24301e01b10
--- /dev/null
+++ b/app/assets/javascripts/copy_to_clipboard.js.coffee
@@ -0,0 +1,37 @@
+#= require clipboard
+
+genericSuccess = (e) ->
+ showTooltip(e.trigger, 'Copied!')
+
+ # Clear the selection and blur the trigger so it loses its border
+ e.clearSelection()
+ $(e.trigger).blur()
+
+# Safari doesn't support `execCommand`, so instead we inform the user to
+# copy manually.
+#
+# See http://clipboardjs.com/#browser-support
+genericError = (e) ->
+ if /Mac/i.test(navigator.userAgent)
+ key = '&#8984;' # Command
+ else
+ key = 'Ctrl'
+
+ showTooltip(e.trigger, "Press #{key}-C to copy")
+
+showTooltip = (target, title) ->
+ $(target).
+ tooltip(
+ container: 'body'
+ html: 'true'
+ placement: 'auto bottom'
+ title: title
+ trigger: 'manual'
+ ).
+ tooltip('show').
+ one('mouseleave', -> $(this).tooltip('hide'))
+
+$ ->
+ clipboard = new Clipboard '[data-clipboard-target], [data-clipboard-text]'
+ clipboard.on 'success', genericSuccess
+ clipboard.on 'error', genericError
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 5bf0b302179..4059fc39c67 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -28,6 +28,8 @@ class Dispatcher
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
new DropzoneInput($('.milestone-form'))
+ when 'groups:milestones:new'
+ new ZenMode()
when 'projects:compare:show'
new Diff()
when 'projects:issues:new','projects:issues:edit'
@@ -39,6 +41,12 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
new DropzoneInput($('.merge-request-form'))
new IssuableForm($('.merge-request-form'))
+ when 'projects:tags:new'
+ new ZenMode()
+ new DropzoneInput($('.tag-form'))
+ when 'projects:releases:edit'
+ new ZenMode()
+ new DropzoneInput($('.release-form'))
when 'projects:merge_requests:show'
new Diff()
shortcut_handler = new ShortcutsIssuable()
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index 6f789e668af..30a35a04339 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -1,3 +1,5 @@
+#= require markdown_preview
+
class @DropzoneInput
constructor: (form) ->
Dropzone.autoDiscover = false
@@ -11,17 +13,14 @@ class @DropzoneInput
uploadProgress = $("<div class=\"div-dropzone-progress\"></div>")
btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>"
project_uploads_path = window.project_uploads_path or null
- markdown_preview_path = window.markdown_preview_path or null
max_file_size = gon.max_file_size or 10
form_textarea = $(form).find("textarea.markdown-area")
form_textarea.wrap "<div class=\"div-dropzone\"></div>"
form_textarea.on 'paste', (event) =>
handlePaste(event)
- form_textarea.on "input", ->
- hideReferencedUsers()
- form_textarea.on "blur", ->
- renderMarkdown()
+
+ $(form).setupMarkdownPreview()
form_dropzone = $(form).find('.div-dropzone')
form_dropzone.parent().addClass "div-dropzone-wrapper"
@@ -34,42 +33,6 @@ class @DropzoneInput
"opacity": 0
"display": "none"
- # Preview button
- $(document).off "click", ".js-md-preview-button"
- $(document).on "click", ".js-md-preview-button", (e) ->
- ###
- Shows the Markdown preview.
-
- Lets the server render GFM into Html and displays it.
- ###
- e.preventDefault()
- form = $(this).closest("form")
- # toggle tabs
- form.find(".js-md-write-button").parent().removeClass "active"
- form.find(".js-md-preview-button").parent().addClass "active"
-
- # toggle content
- form.find(".md-write-holder").hide()
- form.find(".md-preview-holder").show()
-
- renderMarkdown()
-
- # Write button
- $(document).off "click", ".js-md-write-button"
- $(document).on "click", ".js-md-write-button", (e) ->
- ###
- Shows the Markdown textarea.
- ###
- e.preventDefault()
- form = $(this).closest("form")
- # toggle tabs
- form.find(".js-md-write-button").parent().addClass "active"
- form.find(".js-md-preview-button").parent().removeClass "active"
-
- # toggle content
- form.find(".md-write-holder").show()
- form.find(".md-preview-holder").hide()
-
dropzone = form_dropzone.dropzone(
url: project_uploads_path
dictDefaultMessage: ""
@@ -136,41 +99,6 @@ class @DropzoneInput
child = $(dropzone[0]).children("textarea")
- hideReferencedUsers = ->
- referencedUsers = form.find(".referenced-users")
- referencedUsers.hide()
-
- renderReferencedUsers = (users) ->
- referencedUsers = form.find(".referenced-users")
-
- if referencedUsers.length
- if users.length >= 10
- referencedUsers.show()
- referencedUsers.find(".js-referenced-users-count").text users.length
- else
- referencedUsers.hide()
-
- renderMarkdown = ->
- preview = form.find(".js-md-preview")
- mdText = form.find(".markdown-area").val()
- if mdText.trim().length is 0
- preview.text "Nothing to preview."
- hideReferencedUsers()
- else
- preview.text "Loading..."
- $.ajax(
- type: "POST",
- url: markdown_preview_path,
- data: {
- text: mdText
- },
- dataType: "json"
- ).success (data) ->
- preview.html data.body
- preview.syntaxHighlight()
-
- renderReferencedUsers data.references.users
-
formatLink = (link) ->
text = "[#{link.alt}](#{link.url})"
text = "!#{text}" if link.is_image
diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee
index b39ab0c4475..9b59d4e57f7 100644
--- a/app/assets/javascripts/flash.js.coffee
+++ b/app/assets/javascripts/flash.js.coffee
@@ -1,12 +1,16 @@
class @Flash
constructor: (message, type)->
- flash = $(".flash-container")
- flash.html("")
+ @flash = $(".flash-container")
+ @flash.html("")
- $('<div/>',
+ innerDiv = $('<div/>',
class: "flash-#{type}",
text: message
- ).appendTo(".flash-container")
+ )
+ innerDiv.appendTo(".flash-container")
- flash.click -> $(@).fadeOut()
- flash.show()
+ @flash.click -> $(@).fadeOut()
+ @flash.show()
+
+ pin: ->
+ @flash.addClass('flash-pinned flash-raised')
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index 40bb9e9cb0c..ac9e022e727 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -29,7 +29,7 @@
$('#filter_issue_search').val($('#issue_search').val())
initSelects: ->
- $("select#update_status").select2(width: 'resolve', dropdownAutoWidth: true)
+ $("select#update_state_event").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#update_assignee_id").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#update_milestone_id").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#label_name").select2(width: 'resolve', dropdownAutoWidth: true)
diff --git a/app/assets/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee
index e604e6025c2..2254a3f91ae 100644
--- a/app/assets/javascripts/line_highlighter.js.coffee
+++ b/app/assets/javascripts/line_highlighter.js.coffee
@@ -6,7 +6,7 @@
#
# ### Example Markup
#
-# <div id="tree-content-holder">
+# <div id="blob-content-holder">
# <div class="file-content">
# <div class="line-numbers">
# <a href="#L1" id="L1" data-line-number="1">1</a>
@@ -53,7 +53,7 @@ class @LineHighlighter
$.scrollTo("#L#{range[0]}", offset: -150)
bindEvents: ->
- $('#tree-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler
+ $('#blob-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler
# While it may seem odd to bind to the mousedown event and then throw away
# the click event, there is a method to our madness.
@@ -62,7 +62,7 @@ class @LineHighlighter
# active state even when the event is cancelled, resulting in an ugly border
# around the link and/or a persisted underline text decoration.
- $('#tree-content-holder').on 'click', 'a[data-line-number]', (event) ->
+ $('#blob-content-holder').on 'click', 'a[data-line-number]', (event) ->
event.preventDefault()
clickHandler: (event) =>
diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee
new file mode 100644
index 00000000000..98fc8f17340
--- /dev/null
+++ b/app/assets/javascripts/markdown_preview.js.coffee
@@ -0,0 +1,87 @@
+# MarkdownPreview
+#
+# Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
+# and showing a warning when more than `x` users are referenced.
+#
+class @MarkdownPreview
+ # Minimum number of users referenced before triggering a warning
+ referenceThreshold: 10
+
+ showPreview: (form) ->
+ preview = form.find('.js-md-preview')
+ mdText = form.find('textarea.markdown-area').val()
+
+ if mdText.trim().length == 0
+ preview.text('Nothing to preview.')
+ @hideReferencedUsers(form)
+ else
+ preview.text('Loading...')
+ @renderMarkdown mdText, (response) =>
+ preview.html(response.body)
+ preview.syntaxHighlight()
+ @renderReferencedUsers(response.references.users, form)
+
+ renderMarkdown: (text, success) ->
+ return unless window.markdown_preview_path
+
+ $.ajax
+ type: 'POST'
+ url: window.markdown_preview_path
+ data: { text: text }
+ dataType: 'json'
+ success: success
+
+ hideReferencedUsers: (form) ->
+ referencedUsers = form.find('.referenced-users')
+ referencedUsers.hide()
+
+ renderReferencedUsers: (users, form) ->
+ referencedUsers = form.find('.referenced-users')
+
+ if referencedUsers.length
+ if users.length >= @referenceThreshold
+ referencedUsers.show()
+ referencedUsers.find('.js-referenced-users-count').text(users.length)
+ else
+ referencedUsers.hide()
+
+markdownPreview = new MarkdownPreview()
+
+previewButtonSelector = '.js-md-preview-button'
+writeButtonSelector = '.js-md-write-button'
+
+$.fn.setupMarkdownPreview = ->
+ $form = $(this)
+
+ form_textarea = $form.find('textarea.markdown-area')
+
+ form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form)
+ form_textarea.on 'blur', -> markdownPreview.showPreview($form)
+
+$(document).on 'click', previewButtonSelector, (e) ->
+ e.preventDefault()
+
+ $form = $(this).closest('form')
+
+ # toggle tabs
+ $form.find(writeButtonSelector).parent().removeClass('active')
+ $form.find(previewButtonSelector).parent().addClass('active')
+
+ # toggle content
+ $form.find('.md-write-holder').hide()
+ $form.find('.md-preview-holder').show()
+
+ markdownPreview.showPreview($form)
+
+$(document).on 'click', writeButtonSelector, (e) ->
+ e.preventDefault()
+
+ $form = $(this).closest('form')
+
+ # toggle tabs
+ $form.find(writeButtonSelector).parent().addClass('active')
+ $form.find(previewButtonSelector).parent().removeClass('active')
+
+ # toggle content
+ $form.find('.md-write-holder').show()
+ $form.find('.md-preview-holder').hide()
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 19a07b6a033..593a8f42130 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -66,6 +66,11 @@ class @MergeRequestTabs
@setCurrentAction(action)
+ scrollToElement: (container) ->
+ if window.location.hash
+ $el = $("#{container} #{window.location.hash}")
+ $('body').scrollTo($el.offset().top) if $el.length
+
# Activate a tab based on the current action
activateTab: (action) ->
action = 'notes' if action == 'show'
@@ -122,6 +127,7 @@ class @MergeRequestTabs
document.getElementById('commits').innerHTML = data.html
$('.js-timeago').timeago()
@commitsLoaded = true
+ @scrollToElement("#commits")
loadDiff: (source) ->
return if @diffsLoaded
@@ -131,14 +137,18 @@ class @MergeRequestTabs
success: (data) =>
document.getElementById('diffs').innerHTML = data.html
@diffsLoaded = true
+ @scrollToElement("#diffs")
- toggleLoading: ->
- $('.mr-loading-status .loading').toggle()
+ # Show or hide the loading spinner
+ #
+ # status - Boolean, true to show, false to hide
+ toggleLoading: (status) ->
+ $('.mr-loading-status .loading').toggle(status)
_get: (options) ->
defaults = {
- beforeSend: @toggleLoading
- complete: @toggleLoading
+ beforeSend: => @toggleLoading(true)
+ complete: => @toggleLoading(false)
dataType: 'json'
type: 'GET'
}
diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee
index 995a2f24093..c4b63966fe7 100644
--- a/app/assets/javascripts/merge_request_widget.js.coffee
+++ b/app/assets/javascripts/merge_request_widget.js.coffee
@@ -10,16 +10,20 @@ class @MergeRequestWidget
constructor: (@opts) ->
modal = $('#modal_merge_info').modal(show: false)
- mergeInProgress: ->
+ mergeInProgress: (deleteSourceBranch = false)->
$.ajax
type: 'GET'
url: $('.merge-request').data('url')
success: (data) =>
- switch data.state
- when 'merged'
- location.reload()
- else
- setTimeout(merge_request_widget.mergeInProgress, 2000)
+ if data.state == "merged"
+ urlSuffix = if deleteSourceBranch then '?delete_source=true' else ''
+
+ window.location.href = window.location.href + urlSuffix
+ else if data.merge_error
+ $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>")
+ else
+ callback = -> merge_request_widget.mergeInProgress(deleteSourceBranch)
+ setTimeout(callback, 2000)
dataType: 'json'
getMergeStatus: ->
diff --git a/app/assets/javascripts/new_commit_form.js.coffee b/app/assets/javascripts/new_commit_form.js.coffee
new file mode 100644
index 00000000000..2e561dea3e1
--- /dev/null
+++ b/app/assets/javascripts/new_commit_form.js.coffee
@@ -0,0 +1,21 @@
+class @NewCommitForm
+ constructor: (form) ->
+ @newBranch = form.find('.js-new-branch')
+ @originalBranch = form.find('.js-original-branch')
+ @createMergeRequest = form.find('.js-create-merge-request')
+ @createMergeRequestFormGroup = form.find('.js-create-merge-request-form-group')
+
+ @renderDestination()
+ @newBranch.keyup @renderDestination
+
+ renderDestination: =>
+ different = @newBranch.val() != @originalBranch.val()
+
+ if different
+ @createMergeRequestFormGroup.show()
+ @createMergeRequest.prop('checked', true) unless @wasDifferent
+ else
+ @createMergeRequestFormGroup.hide()
+ @createMergeRequest.prop('checked', false)
+
+ @wasDifferent = different
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index ce638c2641b..533d00bfb0c 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -63,12 +63,6 @@ class @Notes
# fetch notes when tab becomes visible
$(document).on "visibilitychange", @visibilityChange
- # Chrome doesn't fire keypress or keyup for Command+Enter, so we need keydown.
- $(document).on 'keydown', '.js-note-text', (e) ->
- return if e.originalEvent.repeat
- if e.keyCode == 10 || ((e.metaKey || e.ctrlKey) && e.keyCode == 13)
- $(@).closest('form').submit()
-
cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form"
@@ -82,7 +76,6 @@ class @Notes
$(document).off "click", ".js-discussion-reply-button"
$(document).off "click", ".js-add-diff-note-button"
$(document).off "visibilitychange"
- $(document).off "keydown", ".js-note-text"
$(document).off "keyup", ".js-note-text"
$(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close"
@@ -118,15 +111,25 @@ class @Notes
Note: for rendering inline notes use renderDiscussionNote
###
renderNote: (note) ->
+ unless note.valid
+ if note.award
+ flash = new Flash('You have already used this award emoji!', 'alert')
+ flash.pin()
+ return
+
# render note if it not present in loaded list
# or skip if rendered
- if @isNewNote(note)
+ if @isNewNote(note) && !note.award
@note_ids.push(note.id)
$('ul.main-notes-list').
append(note.html).
syntaxHighlight()
@initTaskList()
+ if note.award
+ awards_handler.addAwardToEmojiBar(note.note, note.emoji_path)
+ awards_handler.scrollToAwards()
+
###
Check if note does not exists on page
###
@@ -262,7 +265,6 @@ class @Notes
###
addNote: (xhr, note, status) =>
@renderNote(note)
- @updateVotes()
###
Called in response to the new note form being submitted
@@ -277,13 +279,15 @@ class @Notes
Updates the current note field.
###
- updateNote: (xhr, note, status) =>
- note_li = $(".note-row-" + note.id)
- note_li.replaceWith(note.html)
- note_li.find('.note-edit-form').hide()
- note_li.find('.note-body > .note-text').show()
- note_li.find('js-task-list-container').taskList('enable')
- @enableTaskList()
+ updateNote: (_xhr, note, _status) =>
+ # Convert returned HTML to a jQuery object so we can modify it further
+ $html = $(note.html)
+ $html.syntaxHighlight()
+ $html.find('.js-task-list-container').taskList('enable')
+
+ # Find the note's `li` element by ID and replace it with the updated HTML
+ $note_li = $("#note_#{note.id}")
+ $note_li.replaceWith($html)
###
Called in response to clicking the edit note link
@@ -365,8 +369,8 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-attachment").remove()
note.find(".note-body > .note-text").show()
- note.find(".js-note-attachment-delete").hide()
- note.find(".note-edit-form").hide()
+ note.find(".note-header").show()
+ note.find(".current-note-edit-form").remove()
###
Called when clicking on the "reply" button for a diff line.
@@ -478,9 +482,6 @@ class @Notes
form = $(e.target).closest(".js-discussion-note-form")
@removeDiscussionNoteForm(form)
- updateVotes: ->
- true
-
###
Called after an attachment file has been selected.
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
index 0ea8fffce07..ec919f0cd67 100644
--- a/app/assets/javascripts/project.js.coffee
+++ b/app/assets/javascripts/project.js.coffee
@@ -1,13 +1,19 @@
class @Project
constructor: ->
- # Git clone panel switcher
- cloneHolder = $('.git-clone-holder')
- if cloneHolder.length
- $('a, button', cloneHolder).click ->
- $('a, button', cloneHolder).removeClass 'active'
- $(@).addClass 'active'
- $('#project_clone', cloneHolder).val $(@).data 'clone'
- $(".clone").text("").append $(@).data 'clone'
+ # Git protocol switcher
+ $('.js-protocol-switch').click ->
+ return if $(@).hasClass('active')
+
+ # Toggle 'active' for both buttons
+ $('.js-protocol-switch').toggleClass('active')
+
+ url = $(@).data('clone')
+
+ # Update the input field
+ $('#project_clone').val(url)
+
+ # Update the command line instructions
+ $('.clone').text(url)
# Ref switcher
$('.project-refs-select').on 'change', ->
@@ -39,4 +45,4 @@ class @Project
when 4 then label = ' On Mention '
$('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>")
$(@).parents('ul').find('li.active').removeClass 'active'
- $(@).parent().addClass 'active' \ No newline at end of file
+ $(@).parent().addClass 'active'
diff --git a/app/assets/javascripts/shortcuts_navigation.coffee b/app/assets/javascripts/shortcuts_navigation.coffee
index 5b6f9e7e3f2..8decaedd87b 100644
--- a/app/assets/javascripts/shortcuts_navigation.coffee
+++ b/app/assets/javascripts/shortcuts_navigation.coffee
@@ -7,6 +7,7 @@ class @ShortcutsNavigation extends Shortcuts
Mousetrap.bind('g e', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity'))
Mousetrap.bind('g f', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-tree'))
Mousetrap.bind('g c', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-commits'))
+ Mousetrap.bind('g b', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-builds'))
Mousetrap.bind('g n', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-network'))
Mousetrap.bind('g g', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs'))
Mousetrap.bind('g i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-issues'))
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index fb08016fbae..ae59480af9e 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -5,6 +5,7 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
+ $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
)
diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee
index cfe5508290f..f5584bcfe4b 100644
--- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee
+++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee
@@ -6,7 +6,7 @@ window.ContributorsStatGraphUtil =
for entry in log
@add_date(entry.date, total) unless total[entry.date]?
- data = by_author[entry.author_name] #|| by_email[entry.author_email]
+ data = by_author[entry.author_name] || by_email[entry.author_email]
data ?= @add_author(entry, by_author, by_email)
@add_date(entry.date, data) unless data[entry.date]
@@ -95,5 +95,4 @@ window.ContributorsStatGraphUtil =
if date_range is null || date_range[0] <= new Date(date) <= date_range[1]
true
else
- false
-
+ false \ No newline at end of file
diff --git a/app/assets/javascripts/tree.js.coffee b/app/assets/javascripts/tree.js.coffee
index d428db5b422..de8eebcd0b2 100644
--- a/app/assets/javascripts/tree.js.coffee
+++ b/app/assets/javascripts/tree.js.coffee
@@ -16,6 +16,9 @@ class @TreeView
li = $("tr.tree-item")
liSelected = null
$('body').keydown (e) ->
+ if $("input:focus").length > 0 && (e.which == 38 || e.which == 40)
+ return false
+
if e.which is 40
if liSelected
next = liSelected.next()
@@ -38,4 +41,4 @@ class @TreeView
$(liSelected).focus()
else if e.which is 13
path = $('.tree-item.selected .tree-item-file-name a').attr('href')
- Turbolinks.visit(path)
+ if path then Turbolinks.visit(path)
diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee
index d0d81f96921..ec4271b092c 100644
--- a/app/assets/javascripts/user.js.coffee
+++ b/app/assets/javascripts/user.js.coffee
@@ -2,3 +2,9 @@ class @User
constructor: ->
$('.profile-groups-avatars').tooltip("placement": "top")
new ProjectsList()
+
+ $('.hide-project-limit-message').on 'click', (e) ->
+ path = '/'
+ $.cookie('hide_project_limit_message', 'false', { path: path })
+ $(@).parents('.project-limit-message').remove()
+ e.preventDefault()
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 9157562a5c5..12abf806bfa 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -32,17 +32,15 @@ class @UsersSelect
if showNullUser
nullUser = {
name: 'Unassigned',
- avatar: null,
- username: 'none',
id: 0
}
data.results.unshift(nullUser)
if showAnyUser
+ name = showAnyUser
+ name = 'Any User' if name == true
anyUser = {
- name: 'Any',
- avatar: null,
- username: 'none',
+ name: name,
id: null
}
data.results.unshift(anyUser)
@@ -50,7 +48,6 @@ class @UsersSelect
if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/)
emailUser = {
name: "Invite \"#{query.term}\"",
- avatar: null,
username: query.term,
id: query.term
}
@@ -58,11 +55,8 @@ class @UsersSelect
query.callback(data)
- initSelection: (element, callback) =>
- id = $(element).val()
- if id != "" && id != "0"
- @user(id, callback)
-
+ initSelection: (args...) =>
+ @initSelection(args...)
formatResult: (args...) =>
@formatResult(args...)
formatSelection: (args...) =>
@@ -71,16 +65,24 @@ class @UsersSelect
escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results
m
+ initSelection: (element, callback) ->
+ id = $(element).val()
+ if id == "0"
+ nullUser = { name: 'Unassigned' }
+ callback(nullUser)
+ else if id != ""
+ @user(id, callback)
+
formatResult: (user) ->
if user.avatar_url
avatar = user.avatar_url
else
avatar = gon.default_avatar_url
- "<div class='user-result'>
+ "<div class='user-result #{'no-username' unless user.username}'>
<div class='user-image'><img class='avatar s24' src='#{avatar}'></div>
<div class='user-name'>#{user.name}</div>
- <div class='user-username'>#{user.username}</div>
+ <div class='user-username'>#{user.username || ""}</div>
</div>"
formatSelection: (user) ->
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index d9ede637944..7b060ce4853 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -11,59 +11,41 @@
*= require cal-heatmap
*/
+/*
+ * Welcome to GitLab css!
+ * If you need to add or modify UI component that is common for many pages
+ * like a table or typography then make changes in the framework/ directory.
+ * If you need to add unique style that should affect only one page - use pages/
+ * directory.
+ */
-@import "base/fonts";
-@import "base/variables";
-@import "base/mixins";
-@import "base/layout";
-
-
-/**
- * Customized Twitter bootstrap
+/*
+ * GitLab UI framework
*/
-@import 'base/gl_variables';
-@import 'base/gl_bootstrap';
+@import "framework";
-/**
+/*
* NProgress load bar css
*/
@import 'nprogress';
@import 'nprogress-bootstrap';
-/**
+/*
* Font icons
- *
*/
@import "font-awesome";
-/**
- * UI themes:
- */
-@import "themes/**/*";
-
-/**
- * Generic css (forms, nav etc):
- */
-@import "generic/**/*";
-
-/**
+/*
* Page specific styles (issues, projects etc):
*/
-
@import "pages/**/*";
-/**
+/*
* Code highlight
*/
@import "highlight/**/*";
-/**
+/*
* Styles for JS behaviors.
*/
-@import "behaviors.scss";
-
-/**
- * CI specific styles:
- */
-@import "ci/**/*";
-
+@import "behaviors.scss"; \ No newline at end of file
diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss
deleted file mode 100644
index 2fc7bf1720a..00000000000
--- a/app/assets/stylesheets/base/variables.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-$hover: #FFFAF1;
-$gl-text-color: #54565b;
-$gl-header-color: #4c4e54;
-$gl-link-color: #333c48;
-$md-text-color: #444;
-$md-link-color: #3084bb;
-$nprogress-color: #c0392b;
-$gl-font-size: 15px;
-$list-font-size: 15px;
-$sidebar_collapsed_width: 62px;
-$sidebar_width: 230px;
-$avatar_radius: 50%;
-$code_font_size: 13px;
-$code_line_height: 1.5;
-$border-color: #E7E9ED;
-$background-color: #F8FAFC;
-$header-height: 58px;
-$fixed-layout-width: 1200px;
-$gl-gray: #7f8fa4;
-$gl-padding: 16px;
-$gl-avatar-size: 46px;
-
-
-/*
- * State colors:
- */
-$gl-primary: #446e9b;
-$gl-success: #44c679;
-$gl-info: #00aaff;
-$gl-warning: #EB9532;
-$gl-danger: #d9534f;
-
-/*
- * Commit Diff Colors
- */
-$added: #63c363;
-$deleted: #f77;
-
-/*
- * Fonts
- */
-$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;
diff --git a/app/assets/stylesheets/ci/runners.scss b/app/assets/stylesheets/ci/runners.scss
deleted file mode 100644
index 2b15ab83129..00000000000
--- a/app/assets/stylesheets/ci/runners.scss
+++ /dev/null
@@ -1,36 +0,0 @@
-.ci-body {
- .runner-state {
- padding: 6px 12px;
- margin-right: 10px;
- color: #FFF;
-
- &.runner-state-shared {
- background: #32b186;
- }
- &.runner-state-specific {
- background: #3498db;
- }
- }
-
- .runner-status-online {
- color: green;
- }
-
- .runner-status-offline {
- color: gray;
- }
-
- .runner-status-paused {
- color: red;
- }
-
- .runner {
- .btn {
- padding: 1px 6px;
- }
-
- h4 {
- font-weight: normal;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
new file mode 100644
index 00000000000..48a4971c8fc
--- /dev/null
+++ b/app/assets/stylesheets/framework.scss
@@ -0,0 +1,34 @@
+@import "framework/fonts";
+@import "framework/variables";
+@import "framework/mixins";
+@import 'framework/tw_bootstrap_variables';
+@import 'framework/tw_bootstrap';
+@import "framework/layout";
+
+@import "framework/avatar.scss";
+@import "framework/blocks.scss";
+@import "framework/buttons.scss";
+@import "framework/calendar.scss";
+@import "framework/callout.scss";
+@import "framework/common.scss";
+@import "framework/files.scss";
+@import "framework/filters.scss";
+@import "framework/flash.scss";
+@import "framework/forms.scss";
+@import "framework/gfm.scss";
+@import "framework/gitlab-theme.scss";
+@import "framework/header.scss";
+@import "framework/highlight.scss";
+@import "framework/issue_box.scss";
+@import "framework/jquery.scss";
+@import "framework/lists.scss";
+@import "framework/markdown_area.scss";
+@import "framework/mobile.scss";
+@import "framework/pagination.scss";
+@import "framework/panels.scss";
+@import "framework/selects.scss";
+@import "framework/sidebar.scss";
+@import "framework/tables.scss";
+@import "framework/timeline.scss";
+@import "framework/typography.scss";
+@import "framework/zen.scss";
diff --git a/app/assets/stylesheets/generic/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 221cb6a04a5..36e582d4854 100644
--- a/app/assets/stylesheets/generic/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -28,6 +28,7 @@
&.s48 { width: 48px; height: 48px; margin-right: 10px; }
&.s60 { width: 60px; height: 60px; margin-right: 12px; }
&.s90 { width: 90px; height: 90px; margin-right: 15px; }
+ &.s110 { width: 110px; height: 110px; margin-right: 15px; }
&.s140 { width: 140px; height: 140px; margin-right: 20px; }
&.s160 { width: 160px; height: 160px; margin-right: 20px; }
}
@@ -42,6 +43,7 @@
&.s32 { font-size: 22px; line-height: 32px; }
&.s60 { font-size: 32px; line-height: 60px; }
&.s90 { font-size: 36px; line-height: 90px; }
+ &.s110 { font-size: 40px; line-height: 112px; font-weight: 300; }
&.s140 { font-size: 72px; line-height: 140px; }
&.s160 { font-size: 96px; line-height: 160px; }
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
new file mode 100644
index 00000000000..a62c0f62a4c
--- /dev/null
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -0,0 +1,129 @@
+.light-well {
+ background-color: #f8fafc;
+ padding: 15px;
+}
+
+.centered-light-block {
+ text-align: center;
+ color: $gl-gray;
+ margin: 20px;
+}
+
+.nothing-here-block {
+ text-align: center;
+ padding: 20px;
+ color: $gl-gray;
+ font-weight: normal;
+ font-size: 16px;
+ line-height: 36px;
+}
+
+.content-block,
+.gray-content-block {
+ margin: -$gl-padding;
+ background-color: $background-color;
+ padding: $gl-padding;
+ margin-bottom: 0px;
+ border-top: 1px solid $border-color;
+ border-bottom: 1px solid $border-color;
+ color: $gl-gray;
+
+ &.oneline-block {
+ line-height: 42px;
+ }
+
+ &.white {
+ background-color: white;
+ }
+
+ &.top-block {
+ border-top: none;
+ }
+
+ &.middle-block {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+ &.clear-block {
+ margin-bottom: $gl-padding - 1px;
+ padding-bottom: $gl-padding;
+ }
+
+ &.second-block {
+ margin-top: -1px;
+ margin-bottom: 0;
+ }
+
+ &.footer-block {
+ margin-top: 0;
+ border-bottom: none;
+ margin-bottom: -$gl-padding;
+ }
+
+ .title {
+ color: $gl-text-color;
+ }
+
+ .oneline {
+ line-height: 42px;
+ }
+
+ > p:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.cover-block {
+ text-align: center;
+ background: #f7f8fa;
+ margin: -$gl-padding;
+ margin-bottom: 0;
+ padding: 44px $gl-padding;
+ border-bottom: 1px solid $border-color;
+ position: relative;
+
+ .avatar-holder {
+ margin-bottom: 16px;
+
+ .avatar, .identicon {
+ margin: 0 auto;
+ float: none;
+ }
+
+ .identicon {
+ @include border-radius(50%);
+ }
+ }
+
+ .cover-title {
+ color: $gl-header-color;
+ margin: 0;
+ font-size: 23px;
+ font-weight: normal;
+ margin: 16px 0 5px 0;
+ color: #4c4e54;
+ font-size: 23px;
+ line-height: 1.1;
+ }
+
+ .cover-desc {
+ padding: 0 $gl-padding 3px;
+ color: $gl-text-color;
+ }
+
+ .cover-controls {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+
+ &.left {
+ left: 10px;
+ right: auto;
+ }
+ }
+}
+
+.block-connector {
+ margin-top: -1px;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
new file mode 100644
index 00000000000..fe56266284b
--- /dev/null
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -0,0 +1,186 @@
+@mixin btn-default {
+ @include border-radius(2px);
+ border-width: 1px;
+ border-style: solid;
+ text-transform: uppercase;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 18px;
+ padding: 11px $gl-padding;
+ letter-spacing: .4px;
+
+ &:focus,
+ &:active {
+ outline: none;
+ @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+ }
+}
+
+@mixin btn-middle {
+ @include btn-default;
+ @include border-radius(2px);
+ padding: 11px 24px;
+}
+
+@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
+ background-color: $light;
+ border-color: $border-light;
+ color: $color;
+
+ &:hover,
+ &:focus {
+ background-color: $normal;
+ border-color: $border-normal;
+ color: $color;
+ }
+
+ &:active {
+ @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12));
+
+ background-color: $dark;
+ border-color: $border-dark;
+ color: $color;
+ }
+}
+
+@mixin btn-green {
+ @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #FFFFFF);
+}
+
+@mixin btn-blue {
+ @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF);
+}
+
+@mixin btn-orange {
+ @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF);
+}
+
+@mixin btn-red {
+ @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #FFFFFF);
+}
+
+@mixin btn-gray {
+ @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, #313236);
+}
+
+@mixin btn-white {
+ @include btn-color($white-light, $border-white-light, $white-normal, $border-white-normal, $white-dark, $border-white-dark, #313236);
+}
+
+.btn {
+ @include btn-default;
+ @include btn-white;
+
+ &.btn-sm {
+ padding: 5px 10px;
+ }
+
+ &.btn-xs {
+ padding: 1px 5px;
+ }
+
+ &.btn-success,
+ &.btn-new,
+ &.btn-create,
+ &.btn-save,
+ &.btn-green {
+ @include btn-green;
+ }
+
+ &.btn-gray {
+ @include btn-gray;
+ }
+
+ &.btn-primary,
+ &.btn-info {
+ @include btn-blue;
+ }
+
+ &.btn-warning {
+ @include btn-orange;
+ }
+
+ &.btn-danger,
+ &.btn-remove,
+ &.btn-red {
+ @include btn-red;
+ }
+
+ &.btn-cancel {
+ float: right;
+ }
+
+ &.btn-close {
+ color: $gl-danger;
+ border-color: $gl-danger;
+ &:hover {
+ color: #B94A48;
+ }
+ }
+
+ &.btn-reopen {
+ color: $gl-success;
+ border-color: $gl-success;
+ &:hover {
+ color: #468847;
+ }
+ }
+
+ &.btn-grouped {
+ margin-right: 7px;
+ float: left;
+ &:last-child {
+ margin-right: 0px;
+ }
+ }
+}
+
+.btn-block {
+ width: 100%;
+ margin: 0;
+ margin-bottom: 15px;
+ &.btn {
+ padding: 6px 0;
+ }
+}
+
+.btn-group {
+ &.btn-grouped {
+ margin-right: 7px;
+ float: left;
+ &:last-child {
+ margin-right: 0px;
+ }
+ }
+}
+
+.btn-group-next {
+ .btn {
+ padding: 9px 0px;
+ font-size: 15px;
+ color: #7f8fa4;
+ border-color: #e7e9ed;
+ width: 140px;
+
+ .badge {
+ font-weight: normal;
+ background-color: #eee;
+ color: #78a;
+ }
+
+ &.active {
+ border-color: $gl-info;
+ background: $gl-info;
+ color: #fff;
+
+ .badge {
+ color: $gl-info;
+ background-color: white;
+ }
+ }
+ }
+}
+
+.btn-clipboard {
+ border: none;
+}
diff --git a/app/assets/stylesheets/generic/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index a36fefe22c5..a36fefe22c5 100644
--- a/app/assets/stylesheets/generic/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
diff --git a/app/assets/stylesheets/generic/callout.scss b/app/assets/stylesheets/framework/callout.scss
index f1699d21c9b..f3ce4e3c219 100644
--- a/app/assets/stylesheets/generic/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -9,9 +9,9 @@
.bs-callout {
margin: 20px 0;
padding: 20px;
- border-left: 3px solid #eee;
- color: #666;
- background: #f9f9f9;
+ border-left: 3px solid $border-color;
+ color: $text-color;
+ background: $background-color;
}
.bs-callout h4 {
margin-top: 0;
diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/framework/common.scss
index b659717b4e1..d2f491daf78 100644
--- a/app/assets/stylesheets/generic/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -1,13 +1,13 @@
/** COLORS **/
.cgray { color: $gl-gray; }
.clgray { color: #BBB }
-.cred { color: #D12F19 }
-.cgreen { color: #4a2 }
+.cred { color: $gl-text-red; }
+.cgreen { color: $gl-text-green; }
.cdark { color: #444 }
/** COMMON CLASSES **/
.prepend-top-10 { margin-top:10px }
-.prepend-top-default { margin-top: $gl-padding; }
+.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top:20px }
.prepend-left-10 { margin-left:10px }
.prepend-left-20 { margin-left:20px }
@@ -16,6 +16,7 @@
.append-bottom-10 { margin-bottom:10px }
.append-bottom-15 { margin-bottom:15px }
.append-bottom-20 { margin-bottom:20px }
+.append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block }
.center { text-align: center }
@@ -51,6 +52,10 @@ pre {
}
}
+hr {
+ margin: $gl-padding 0;
+}
+
.dropdown-menu > li > a {
text-shadow: none;
}
@@ -63,7 +68,7 @@ pre {
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background: $gl-primary;
- color: #FFF
+ color: #FFF;
}
.str-truncated {
@@ -313,7 +318,7 @@ table {
}
.wiki .highlight, .note-body .highlight {
- margin-bottom: 9px;
+ margin: 12px 0 12px 0;
}
.wiki .code {
@@ -327,15 +332,15 @@ table {
}
}
+.well {
+ margin-bottom: 0;
+}
+
.search_box {
@extend .well;
text-align: center;
}
-.task-status {
- margin-left: 10px;
-}
-
#nprogress .spinner {
top: 15px !important;
right: 10px !important;
@@ -381,6 +386,40 @@ table {
&.no-bottom {
margin-bottom: 0;
}
+
+ &.no-top {
+ margin-top: 0;
+ }
+}
+
+.center-middle-menu {
+ @include nav-menu;
+ padding: 0;
+ text-align: center;
+ margin: -$gl-padding;
+ margin-top: 0;
+ margin-bottom: 0;
+ height: 58px;
+ border-bottom: 1px solid $border-color;
+
+ li {
+ &:after {
+ content: "|";
+ color: $border-gray-light;
+ }
+
+ &:last-child {
+ &:after {
+ content: none;
+ }
+ }
+
+ > a {
+ display: inline-block;
+ text-transform: uppercase;
+ font-size: 13px;
+ }
+ }
}
.dropzone .dz-preview .dz-progress {
@@ -390,3 +429,11 @@ table {
.dropzone .dz-preview .dz-progress .dz-upload {
background: $gl-success !important;
}
+
+.space-right {
+ margin-right: 10px;
+}
+
+.alert, .progress {
+ margin-bottom: $gl-padding;
+}
diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/framework/files.scss
index 9dd77747884..6bf2857e83a 100644
--- a/app/assets/stylesheets/generic/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -8,7 +8,10 @@
border: none;
border-top: 1px solid #E7E9EE;
border-bottom: 1px solid #E7E9EE;
- margin-bottom: 1em;
+
+ &.readme-holder {
+ border-bottom: 0;
+ }
table {
@extend .table;
@@ -21,7 +24,7 @@
text-shadow: 0 1px 1px #fff;
margin: 0;
text-align: left;
- padding: 10px 15px;
+ padding: 10px $gl-padding;
.file-actions {
float: right;
@@ -94,7 +97,6 @@
border-right: none;
}
background: #fff;
- padding: 10px $gl-padding;
}
.lines {
pre {
@@ -168,4 +170,3 @@
}
}
}
-
diff --git a/app/assets/stylesheets/generic/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 8e6922c9231..8e6922c9231 100644
--- a/app/assets/stylesheets/generic/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
diff --git a/app/assets/stylesheets/generic/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 82eb50ad4be..1b723021d76 100644
--- a/app/assets/stylesheets/generic/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -15,3 +15,13 @@
@extend .alert-danger;
}
}
+
+.flash-pinned {
+ position: fixed;
+ top: 80px;
+ width: 80%;
+}
+
+.flash-raised {
+ z-index: 1000;
+}
diff --git a/app/assets/stylesheets/base/fonts.scss b/app/assets/stylesheets/framework/fonts.scss
index e214567eca1..e214567eca1 100644
--- a/app/assets/stylesheets/base/fonts.scss
+++ b/app/assets/stylesheets/framework/fonts.scss
diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 4282832e2bf..032d343df44 100644
--- a/app/assets/stylesheets/generic/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -22,19 +22,14 @@ input[type='text'].danger {
}
.form-actions {
- padding: 17px 20px 18px;
- margin-top: 18px;
- margin-bottom: 18px;
+ margin: -$gl-padding;
+ margin-top: 0;
+ margin-bottom: -$gl-padding;
+ padding: $gl-padding;
background-color: $background-color;
border-top: 1px solid $border-color;
}
-@media (min-width: $screen-sm-min) {
- .form-actions {
- padding-left: 17%;
- }
-}
-
label {
&.control-label {
@extend .col-sm-2;
@@ -79,8 +74,36 @@ label {
.form-control {
@include box-shadow(none);
+ height: 42px;
+ padding: 8px $gl-padding;
}
.wiki-content {
margin-top: 35px;
}
+
+.form-group .control-label {
+ font-weight: normal;
+}
+
+.form-control::-webkit-input-placeholder {
+ color: #7f8fa4;
+}
+
+.input-group {
+ .select2-container {
+ display: table-cell;
+ width: 200px !important;
+ }
+ .input-group-addon {
+ background-color: #f7f8fa;
+ }
+ .input-group-addon:not(:first-child):not(:last-child) {
+ border-left: 0;
+ border-right: 0;
+ }
+}
+
+.help-block {
+ margin-bottom: 0;
+}
diff --git a/app/assets/stylesheets/generic/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index bd9200ace23..5ae0520fd7b 100644
--- a/app/assets/stylesheets/generic/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -22,4 +22,5 @@
.gfm-commit, .gfm-commit_range {
font-family: $monospace_font;
+ font-size: 90%;
}
diff --git a/app/assets/stylesheets/themes/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 8d9a0aae568..8d9a0aae568 100644
--- a/app/assets/stylesheets/themes/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
diff --git a/app/assets/stylesheets/generic/header.scss b/app/assets/stylesheets/framework/header.scss
index b758a526fbb..4dbbb56104b 100644
--- a/app/assets/stylesheets/generic/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -6,15 +6,17 @@ header {
transition-duration: .3s;
&.navbar-empty {
+ height: 58px;
background: #FFF;
border-bottom: 1px solid #EEE;
.center-logo {
- margin: 8px 0;
+ margin: 11px 0;
text-align: center;
- img {
- height: 32px;
+ #tanuki-logo, img {
+ width: 36px;
+ height: 36px;
}
}
}
@@ -26,7 +28,6 @@ header {
min-height: $header-height;
background-color: #fff;
border: none;
- border-bottom: 1px solid #EEE;
.container-fluid {
width: 100% !important;
@@ -51,15 +52,17 @@ header {
.navbar-toggle {
color: #666;
- margin: 0;
+ margin: 6px 0;
border-radius: 0;
position: absolute;
right: 2px;
- top: 15px;
&:hover {
background-color: #EEE;
}
+ &.active {
+ color: #7f8fa4;
+ }
}
}
}
@@ -88,6 +91,7 @@ header {
.navbar-collapse {
float: right;
+ border-top: none;
}
}
@@ -116,6 +120,10 @@ header {
}
}
}
+
+ .impersonation i {
+ color: $red-normal;
+ }
}
@mixin collapsed-header {
diff --git a/app/assets/stylesheets/generic/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 2e13ee842e0..2e13ee842e0 100644
--- a/app/assets/stylesheets/generic/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
diff --git a/app/assets/stylesheets/generic/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index b1fb87a6830..f12d68b5a1f 100644
--- a/app/assets/stylesheets/generic/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -5,10 +5,11 @@
*/
.issue-box {
- @include border-radius(3px);
+ @include border-radius(2px);
- display: inline-block;
- padding: 10px $gl-padding;
+ display: block;
+ float: left;
+ padding: 0 $gl-padding;
font-weight: normal;
margin-right: 10px;
font-size: $gl-font-size;
diff --git a/app/assets/stylesheets/generic/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index 871b808bad4..871b808bad4 100644
--- a/app/assets/stylesheets/generic/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/framework/layout.scss
index b91c15d8910..aa5acb93cc5 100644
--- a/app/assets/stylesheets/base/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -2,9 +2,13 @@ html {
overflow-y: scroll;
&.touch .tooltip { display: none !important; }
+}
+
+body {
+ background-color: #EAEBEC !important;
- body {
- padding-top: $header-height;
+ &.navless {
+ background-color: white !important;
}
}
@@ -18,7 +22,8 @@ html {
}
.navless-container {
- margin-top: 30px;
+ margin-top: $header-height;
+ padding-top: $gl-padding * 2;
}
.container-limited {
diff --git a/app/assets/stylesheets/generic/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 3bfed8de772..a798ae812e3 100644
--- a/app/assets/stylesheets/generic/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -107,7 +107,7 @@ ul.content-list {
> li {
padding: $gl-padding;
- border-color: #f1f2f4;
+ border-color: $table-border-color;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
color: $gl-gray;
@@ -117,9 +117,18 @@ ul.content-list {
}
.controls {
- padding-top: 10px;
+ padding-top: 1px;
float: right;
+
+ .btn {
+ padding: 10px 14px;
+ }
}
}
}
+.panel > .content-list {
+ li {
+ margin: 0;
+ }
+}
diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index ed0333d2336..2b044786738 100644
--- a/app/assets/stylesheets/generic/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -73,11 +73,8 @@
}
.referenced-users {
- padding: 10px 0;
- color: #999;
- margin-left: 10px;
- margin-top: 1px;
- margin-right: 130px;
+ color: #4c4e54;
+ padding-top: 10px;
}
.md-preview-holder {
@@ -106,6 +103,7 @@
}
.markdown-area {
+ @include border-radius(0);
background: #FFF;
border: 1px solid #ddd;
min-height: 140px;
diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 421588f64d8..11c48d26ab5 100644
--- a/app/assets/stylesheets/base/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -54,146 +54,6 @@
@include box-shadow(0 0 0 3px #f1f1f1);
}
-@mixin md-typography {
- color: $md-text-color;
-
- a {
- color: $md-link-color;
- }
-
- img {
- max-width: 100%;
- }
-
- *:first-child {
- margin-top: 0;
- }
-
- code {
- font-family: $monospace_font;
- white-space: pre;
- word-wrap: normal;
- padding: 1px 2px;
- }
-
- kbd {
- display: inline-block;
- padding: 3px 5px;
- font-size: 11px;
- line-height: 10px;
- color: #555;
- vertical-align: middle;
- background-color: #FCFCFC;
- border-width: 1px;
- border-style: solid;
- border-color: #CCC #CCC #BBB;
- border-image: none;
- border-radius: 3px;
- box-shadow: 0px -1px 0px #BBB inset;
- }
-
- h1 {
- font-size: 1.3em;
- font-weight: 600;
- margin: 24px 0 12px 0;
- padding: 0 0 10px 0;
- border-bottom: 1px solid #e7e9ed;
- color: #313236;
- }
-
- h2 {
- font-size: 1.2em;
- font-weight: 600;
- margin: 24px 0 12px 0;
- color: #313236;
- }
-
- h3 {
- margin: 24px 0 12px 0;
- font-size: 1.25em;
- }
-
- h4 {
- margin: 24px 0 12px 0;
- font-size: 1.1em;
- }
-
- h5 {
- margin: 24px 0 12px 0;
- font-size: 1em;
- }
-
- h6 {
- margin: 24px 0 12px 0;
- font-size: 0.90em;
- }
-
- blockquote {
- padding: 8px 21px;
- margin: 12px 0 12px;
- border-left: 3px solid #e7e9ed;
- }
-
- blockquote p {
- color: #7f8fa4 !important;
- font-size: 15px;
- line-height: 1.5;
- }
-
- p {
- color:#5c5d5e;
- margin:6px 0 0 0;
- }
-
- table {
- @extend .table;
- @extend .table-bordered;
- margin: 12px 0 12px 0;
- color: #5c5d5e;
- th {
- background: #f8fafc;
- }
- }
-
- pre {
- margin: 12px 0 12px 0 !important;
- background-color: #f8fafc !important;
- font-size: 13px !important;
- color: #5b6169 !important;
- line-height: 1.6em !important;
- }
-
- p > code {
- font-weight: inherit;
- }
-
-
- ul {
- color: #5c5d5e;
- }
-
- li {
- line-height: 1.6em;
- }
-
- a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] {
- &:before {
- margin-right: 4px;
-
- font: normal normal normal 14px/1 FontAwesome;
- font-size: inherit;
- text-rendering: auto;
- -webkit-font-smoothing: antialiased;
- content: "\f0c6";
- }
-
- &:hover:before {
- text-decoration: none;
- }
- }
-}
-
-
@mixin str-truncated($max_width: 82%) {
display: inline-block;
overflow: hidden;
@@ -212,9 +72,10 @@
list-style: none;
> li {
+ @include clearfix;
+
padding: 10px 0;
border-bottom: 1px solid #EEE;
- overflow: hidden;
display: block;
margin: 0px;
@@ -277,6 +138,7 @@
&:hover, &:active, &:focus {
text-decoration: none;
+ outline: none;
}
}
@@ -287,7 +149,6 @@
.badge {
font-weight: normal;
- background-color: #fff;
background-color: #eee;
color: #78a;
}
diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 36ae126f865..cea47fba192 100644
--- a/app/assets/stylesheets/generic/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -23,7 +23,7 @@
margin-right: 0;
}
- .issues-filters,
+ .issues-details-filters,
.dash-projects-filters,
.check-all-holder {
display: none;
@@ -83,6 +83,7 @@
.center-top-menu {
height: 45px;
+ margin-bottom: 30px;
li a {
font-size: 14px;
@@ -90,9 +91,11 @@
}
}
- .projects-search-form {
- margin: 0 -5px !important;
+ .activity-filter-block {
+ display: none;
+ }
+ .projects-search-form {
.btn {
display: none;
}
@@ -100,6 +103,11 @@
}
@media (max-width: $screen-sm-max) {
+ .page-with-sidebar .content-wrapper {
+ padding: 0;
+ padding-top: 1px;
+ }
+
.issues-filters {
.milestone-filter, .labels-filter {
display: none;
diff --git a/app/assets/stylesheets/generic/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index 6677f94dafd..2cd30491bf5 100644
--- a/app/assets/stylesheets/generic/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -32,3 +32,7 @@
}
}
}
+
+.panel > .gl-pagination {
+ margin: 0;
+}
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
new file mode 100644
index 00000000000..406aff3d72c
--- /dev/null
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -0,0 +1,15 @@
+.panel {
+ margin-bottom: $gl-padding;
+
+ .panel-heading {
+ padding: 10px $gl-padding;
+ }
+ .panel-body {
+ padding: $gl-padding;
+
+ .form-actions {
+ margin: -$gl-padding;
+ margin-top: $gl-padding;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
new file mode 100644
index 00000000000..c01e1e32e41
--- /dev/null
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -0,0 +1,184 @@
+/** Select2 selectbox style override **/
+.select2-container, .select2-container.select2-drop-above {
+ .select2-choice {
+ background: #FFF;
+ border-color: #DDD;
+ height: 42px;
+ padding: 8px $gl-padding;
+ font-size: $gl-font-size;
+ line-height: 1.42857143;
+
+ @include border-radius(2px);
+
+ .select2-arrow {
+ background: #FFF;
+ border-left: none;
+ padding-top: 5px;
+ }
+
+ .select2-chosen {
+ color: $gl-text-color;
+ }
+
+ &.select2-default {
+ .select2-chosen {
+ color: #999;
+ }
+ }
+ }
+}
+
+.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;
+}
+
+.select2-results .select2-result-label {
+ padding: 9px;
+}
+
+.select2-drop{
+ color: #7f8fa4;
+}
+
+.select2-highlighted {
+ background: #3084bb !important;
+}
+
+.select2-results li.select2-result-with-children > .select2-result-label {
+ font-weight: 600;
+ color: #313236;
+}
+
+.select2-container-multi {
+ .select2-choices {
+ @include border-radius(2px);
+ border-color: $input-border;
+ background: white;
+ padding-left: $gl-padding / 2;
+
+ .select2-search-field input {
+ padding: $gl-padding / 2;
+ font-size: 13px;
+ height: auto;
+ font-family: inherit;
+ font-size: inherit;
+ }
+
+ .select2-search-choice {
+ margin: 8px 0 0 8px;
+ background: white;
+ box-shadow: none;
+ border-color: $input-border;
+ color: $gl-text-color;
+ line-height: 15px;
+
+ .select2-search-choice-close {
+ top: 5px;
+ }
+
+ &.select2-search-choice-focus {
+ border-color: $gl-text-color;
+ }
+ }
+ }
+}
+
+.select2-drop-active {
+ border: 1px solid #BBB !important;
+ margin-top: 4px;
+ font-size: 13px;
+
+ &.select2-drop-above {
+ margin-bottom: 8px;
+ }
+
+ .select2-search input {
+ background: #fafafa;
+ border-color: #DDD;
+ }
+
+ .select2-results {
+ max-height: 350px;
+ .select2-highlighted {
+ background: $gl-primary;
+ }
+ }
+}
+
+.select2-container {
+ width: 100% !important;
+}
+
+/** Branch/tag selector **/
+.project-refs-form .select2-container {
+ width: 160px !important;
+}
+
+.ajax-users-dropdown, .ajax-project-users-dropdown {
+ .select2-search {
+ padding-top: 2px;
+ }
+}
+
+.ajax-users-select {
+ width: 400px;
+
+ &.input-large {
+ width: 210px;
+ }
+
+ &.input-clamp {
+ max-width: 100%;
+ }
+}
+
+.group-result {
+ .group-image {
+ float: left;
+ }
+ .group-name {
+ font-weight: bold;
+ }
+ .group-path {
+ color: #999;
+ }
+}
+
+.user-result {
+ min-height: 24px;
+
+ .user-image {
+ float: left;
+ }
+
+ &.no-username {
+ .user-name {
+ line-height: 24px;
+ }
+ }
+}
+
+.namespace-result {
+ .namespace-kind {
+ color: #AAA;
+ font-weight: normal;
+ }
+ .namespace-path {
+ margin-left: 10px;
+ font-weight: bolder;
+ }
+}
+
+.ajax-users-dropdown {
+ min-width: 250px !important;
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 3d055f0e66f..458af76cb75 100644
--- a/app/assets/stylesheets/generic/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,4 +1,7 @@
.page-with-sidebar {
+ padding-top: $header-height;
+ transition-duration: .3s;
+
.sidebar-wrapper {
position: fixed;
top: 0;
@@ -14,20 +17,15 @@
.sidebar-wrapper {
z-index: 99;
background: $background-color;
- transition-duration: .3s;
}
.content-wrapper {
- min-height: 100vh;
width: 100%;
padding: 20px;
- background: #f1f4f8;
.container-fluid {
background: #FFF;
padding: $gl-padding;
- border: 1px solid #e7e9ed;
- min-height: 90vh;
&.container-blank {
background: none;
@@ -37,6 +35,83 @@
}
}
+.sidebar-wrapper {
+ .header-logo {
+ border-bottom: 1px solid transparent;
+ float: left;
+ height: $header-height;
+ width: $sidebar_width;
+ position: fixed;
+ z-index: 999;
+ overflow: hidden;
+ transition-duration: .3s;
+
+ a {
+ float: left;
+ height: $header-height;
+ width: 100%;
+ padding: 11px 0 11px 22px;
+ overflow: hidden;
+ outline: none;
+ transition-duration: .3s;
+
+ img {
+ width: 36px;
+ height: 36px;
+ }
+
+ #tanuki-logo, img {
+ float: left;
+ }
+
+ .gitlab-text-container {
+ width: 230px;
+
+ h3 {
+ width: 158px;
+ float: left;
+ margin: 0;
+ margin-left: 14px;
+ font-size: 19px;
+ line-height: 41px;
+ font-weight: normal;
+ }
+ }
+ }
+
+ &:hover {
+ background-color: #EEE;
+ }
+ }
+
+ .sidebar-user {
+ padding: 9px 22px;
+ position: fixed;
+ bottom: 40px;
+ width: $sidebar_width;
+ overflow: hidden;
+ transition-duration: .3s;
+
+ .username {
+ margin-left: 10px;
+ width: $sidebar_width - 2 * 10px;
+ font-size: 16px;
+ line-height: 34px;
+ }
+ }
+}
+
+
+.tanuki-shape {
+ transition: all 0.8s;
+
+ &:hover {
+ fill: rgb(255, 255, 255);
+ transition: all 0.1s;
+ }
+}
+
+
.nav-sidebar {
margin-top: 14 + $header-height;
margin-bottom: 100px;
@@ -63,8 +138,9 @@
color: $gray;
display: block;
text-decoration: none;
- padding-left: 22px;
+ padding-left: 23px;
font-weight: normal;
+ outline: none;
&:hover {
text-decoration: none;
@@ -86,6 +162,10 @@
padding: 0px 8px;
@include border-radius(6px);
}
+
+ &.back-link i {
+ transition-duration: .3s;
+ }
}
}
}
@@ -101,7 +181,6 @@
@mixin expanded-sidebar {
padding-left: $sidebar_width;
- transition-duration: .3s;
.sidebar-wrapper {
width: $sidebar_width;
@@ -115,16 +194,15 @@
&.back-link {
i {
- visibility: hidden;
+ opacity: 0;
}
}
}
}
}
-@mixin folded-sidebar {
- padding-left: 60px;
- transition-duration: .3s;
+@mixin collapsed-sidebar {
+ padding-left: $sidebar_collapsed_width;
.sidebar-wrapper {
width: $sidebar_collapsed_width;
@@ -133,7 +211,7 @@
width: $sidebar_collapsed_width;
a {
- padding-left: 12px;
+ padding-left: ($sidebar_collapsed_width - 36) / 2;
.gitlab-text-container {
display: none;
@@ -144,9 +222,13 @@
.nav-sidebar {
width: $sidebar_collapsed_width;
- li a {
- span {
- display: none;
+ li {
+ width: auto;
+
+ a {
+ span {
+ display: none;
+ }
}
}
}
@@ -156,7 +238,7 @@
}
.sidebar-user {
- padding-left: 12px;
+ padding-left: ($sidebar_collapsed_width - 36) / 2;
width: $sidebar_collapsed_width;
.username {
@@ -177,6 +259,7 @@
text-align: center;
line-height: 40px;
transition-duration: .3s;
+ outline: none;
}
.collapse-nav a:hover {
@@ -186,11 +269,11 @@
@media (max-width: $screen-md-max) {
.page-sidebar-collapsed {
- @include folded-sidebar;
+ @include collapsed-sidebar;
}
.page-sidebar-expanded {
- @include folded-sidebar;
+ @include collapsed-sidebar;
}
.collapse-nav {
@@ -200,69 +283,10 @@
@media(min-width: $screen-md-max) {
.page-sidebar-collapsed {
- @include folded-sidebar;
+ @include collapsed-sidebar;
}
.page-sidebar-expanded {
@include expanded-sidebar;
}
}
-
-.sidebar-user {
- padding: 9px 22px;
- position: fixed;
- bottom: 40px;
- width: $sidebar_width;
- overflow: hidden;
- transition-duration: .3s;
-
- .username {
- margin-left: 10px;
- width: $sidebar_width - 2 * 10px;
- font-size: 16px;
- line-height: 34px;
- }
-}
-
-.sidebar-wrapper {
- .header-logo {
- border-bottom: 1px solid transparent;
- float: left;
- height: $header-height;
- width: $sidebar_width;
- overflow: hidden;
- transition-duration: .3s;
-
- a {
- float: left;
- height: $header-height;
- width: 100%;
- padding: 10px 22px;
- overflow: hidden;
-
- img {
- width: 36px;
- height: 36px;
- float: left;
- }
-
- .gitlab-text-container {
- width: 230px;
-
- h3 {
- width: 158px;
- float: left;
- margin: 0;
- margin-left: 14px;
- font-size: 19px;
- line-height: 41px;
- font-weight: normal;
- }
- }
- }
-
- &:hover {
- background-color: #EEE;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
new file mode 100644
index 00000000000..793ab3d9bb9
--- /dev/null
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -0,0 +1,46 @@
+.table-holder {
+ margin: -$gl-padding;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+table {
+ &.table {
+ margin-bottom: $gl-padding;
+
+ .dropdown-menu a {
+ text-decoration: none;
+ }
+
+ .success,
+ .warning,
+ .danger,
+ .info {
+ color: #fff;
+
+ a:not(.btn) {
+ text-decoration: underline;
+ color: #fff;
+ }
+ }
+
+ tr {
+ td, th {
+ padding: 10px $gl-padding;
+ line-height: 20px;
+ vertical-align: middle;
+ }
+
+ th {
+ font-weight: normal;
+ font-size: 15px;
+ border-bottom: 1px solid $border-color !important;
+ }
+
+ td {
+ border-color: $table-border-color !important;
+ border-bottom: 1px solid;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/generic/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 74bbaabad39..eb53c4153d3 100644
--- a/app/assets/stylesheets/generic/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -6,12 +6,16 @@
.timeline-entry {
padding: $gl-padding;
- border-color: #f1f2f4;
+ border-color: $table-border-color;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
color: $gl-gray;
- border-bottom: 1px solid #f1f2f4;
- border-right: 1px solid #f1f2f4;
+ border-bottom: 1px solid #ECEEF1;
+ border-right: 1px solid #ECEEF1;
+
+ &:target {
+ background: $hover;
+ }
&:last-child {
border-bottom: none;
diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index eb8d23d6453..94f0ed761df 100644
--- a/app/assets/stylesheets/base/gl_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -32,8 +32,6 @@
@import "bootstrap/pager";
@import "bootstrap/labels";
@import "bootstrap/badges";
-@import "bootstrap/jumbotron";
-@import "bootstrap/thumbnails";
@import "bootstrap/alerts";
@import "bootstrap/progress-bars";
@import "bootstrap/list-group";
@@ -174,7 +172,7 @@
}
.panel-body {
- form {
+ form, pre {
margin: 0;
}
@@ -192,6 +190,10 @@
.btn {
min-width: 124px;
}
+
+ .btn-clipboard {
+ min-width: 0px;
+ }
}
&.panel-small {
@@ -251,23 +253,3 @@
.text-info:hover {
color: $brand-info;
}
-
-// Tables =====================================================================
-
-table.table {
- .dropdown-menu a {
- text-decoration: none;
- }
-
- .success,
- .warning,
- .danger,
- .info {
- color: #fff;
-
- a:not(.btn) {
- text-decoration: underline;
- color: #fff;
- }
- }
-}
diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 7378d404008..63868a34e2a 100644
--- a/app/assets/stylesheets/base/gl_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -22,8 +22,8 @@ $brand-info: $gl-info;
$brand-warning: $gl-warning;
$brand-danger: $gl-danger;
-$border-radius-base: 3px !default;
-$border-radius-large: 5px !default;
+$border-radius-base: 2px !default;
+$border-radius-large: 2px !default;
$border-radius-small: 2px !default;
@@ -156,3 +156,5 @@ $nav-link-padding: 13px $gl-padding;
$pre-bg: #f8fafc !default;
$pre-color: $gl-gray !default;
$pre-border-color: #e7e9ed;
+
+$table-bg-accent: $background-color;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
new file mode 100644
index 00000000000..aef338cfa56
--- /dev/null
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -0,0 +1,268 @@
+@mixin md-typography {
+ color: $md-text-color;
+ word-wrap: break-word;
+
+ a {
+ color: $md-link-color;
+ }
+
+ img {
+ max-width: 100%;
+ }
+
+ *:first-child {
+ margin-top: 0;
+ }
+
+ code {
+ font-family: $monospace_font;
+ white-space: pre;
+ word-wrap: normal;
+ }
+
+ kbd {
+ display: inline-block;
+ padding: 3px 5px;
+ font-size: 11px;
+ line-height: 10px;
+ color: #555;
+ vertical-align: middle;
+ background-color: #FCFCFC;
+ border-width: 1px;
+ border-style: solid;
+ border-color: #CCC #CCC #BBB;
+ border-image: none;
+ border-radius: 3px;
+ box-shadow: 0px -1px 0px #BBB inset;
+ }
+
+ h1 {
+ font-size: 1.3em;
+ font-weight: 600;
+ margin: 24px 0 12px 0;
+ padding: 0 0 10px 0;
+ border-bottom: 1px solid #e7e9ed;
+ color: #313236;
+ }
+
+ h2 {
+ font-size: 1.2em;
+ font-weight: 600;
+ margin: 24px 0 12px 0;
+ color: #313236;
+ }
+
+ h3 {
+ margin: 24px 0 12px 0;
+ font-size: 1.25em;
+ }
+
+ h4 {
+ margin: 24px 0 12px 0;
+ font-size: 1.1em;
+ }
+
+ h5 {
+ margin: 24px 0 12px 0;
+ font-size: 1em;
+ }
+
+ h6 {
+ margin: 24px 0 12px 0;
+ font-size: 0.90em;
+ }
+
+ blockquote {
+ color: #7f8fa4;
+ font-size: inherit;
+ padding: 8px 21px;
+ margin: 12px 0 12px;
+ border-left: 3px solid #e7e9ed;
+ }
+
+ blockquote p {
+ color: #7f8fa4 !important;
+ font-size: inherit;
+ line-height: 1.5;
+ }
+
+ p {
+ color:#5c5d5e;
+ margin:6px 0 0 0;
+ }
+
+ table {
+ @extend .table;
+ @extend .table-bordered;
+ margin: 12px 0 12px 0;
+ color: #5c5d5e;
+ th {
+ background: #f8fafc;
+ }
+ }
+
+ pre {
+ margin: 12px 0 12px 0 !important;
+ background-color: #f8fafc;
+ font-size: 13px !important;
+ color: #5b6169;
+ line-height: 1.6em !important;
+ @include border-radius(2px);
+ }
+
+ p > code {
+ font-weight: inherit;
+ }
+
+ ul, ol {
+ padding: 0;
+ margin: 6px 0 6px 18px !important;
+ }
+
+ li {
+ line-height: 1.6em;
+ }
+
+ a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] {
+ &:before {
+ margin-right: 4px;
+
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ content: "\f0c6";
+ }
+
+ &:hover:before {
+ text-decoration: none;
+ }
+ }
+
+ /* Link to current header. */
+ h1, h2, h3, h4, h5, h6 {
+ position: relative;
+
+ a.anchor {
+ // Setting `display: none` would prevent the anchor being scrolled to, so
+ // instead we set the height to 0 and it gets updated on hover.
+ height: 0;
+ }
+
+ &:hover > a.anchor {
+ $size: 16px;
+ position: absolute;
+ right: 100%;
+ top: 50%;
+ margin-top: -$size/2;
+ margin-right: 0px;
+ padding-right: 20px;
+ display: inline-block;
+ width: $size;
+ height: $size;
+ background-image: image-url("icon-link.png");
+ background-size: contain;
+ background-repeat: no-repeat;
+ }
+ }
+}
+
+
+/**
+ * Headers
+ *
+ */
+body {
+ -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px;
+}
+
+.page-title {
+ margin-top: 0px;
+ line-height: 1.3;
+ font-size: 1.25em;
+ font-weight: 600;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.page-title-empty {
+ margin-top: 0px;
+ line-height: 1.3;
+ font-size: 1.25em;
+ font-weight: 600;
+ margin: 12px 7px 12px 7px;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: $gl-header-color;
+ font-weight: 500;
+}
+
+/** CODE **/
+pre {
+ font-family: $monospace_font;
+
+ &.dark {
+ background: #333;
+ color: $background-color;
+ }
+
+ &.plain-readme {
+ background: none;
+ border: none;
+ padding: 0;
+ margin: 0;
+ font-size: 14px;
+ }
+}
+
+.monospace {
+ font-family: $monospace_font;
+}
+
+code {
+ &.key-fingerprint {
+ background: $body-bg;
+ color: $text-color;
+ }
+}
+
+a > code {
+ color: $link-color;
+}
+
+/**
+ * Apply Markdown typography
+ *
+ */
+.wiki {
+ @include md-typography;
+}
+
+.md {
+ @include md-typography;
+}
+
+/**
+ * Textareas intended for GFM
+ *
+ */
+textarea.js-gfm-input {
+ font-family: $monospace_font;
+ color: $gl-text-color;
+}
+
+.md-preview {
+}
+
+.strikethrough {
+ text-decoration: line-through;
+}
+
+h1, h2, h3, h4 {
+ small {
+ color: $gl-gray;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
new file mode 100644
index 00000000000..91954683c3e
--- /dev/null
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -0,0 +1,99 @@
+$hover: #FFFAF1;
+$gl-text-color: #54565B;
+$gl-text-green: #4A2;
+$gl-text-red: #D12F19;
+$gl-text-orange: #D90;
+$gl-header-color: #4c4e54;
+$gl-link-color: #333c48;
+$md-text-color: #444;
+$md-link-color: #3084bb;
+$nprogress-color: #c0392b;
+$gl-font-size: 15px;
+$list-font-size: 15px;
+$sidebar_collapsed_width: 62px;
+$sidebar_width: 230px;
+$avatar_radius: 50%;
+$code_font_size: 13px;
+$code_line_height: 1.5;
+$border-color: #dce0e6;
+$table-border-color: #eef0f2;
+$background-color: #F7F8FA;
+$header-height: 58px;
+$fixed-layout-width: 1200px;
+$gl-gray: #7f8fa4;
+$gl-padding: 16px;
+$gl-avatar-size: 46px;
+
+/*
+ * Color schema
+ */
+
+$white-light: #FFFFFF;
+$white-normal: #DCE0E5;
+$white-dark: #E4E7ED;
+
+$gray-light: #F0F2F5;
+$gray-normal: #DCE0E5;
+$gray-dark: #E4E7ED;
+
+$green-light: #31AF64;
+$green-normal: #2FAA60;
+$green-dark: #2CA05B;
+
+$blue-light: #2EA8E5;
+$blue-normal: #2D9FD8;
+$blue-dark: #2897CE;
+
+$orange-light: #FC6443;
+$orange-normal: #E75E40;
+$orange-dark: #CE5237;
+
+$red-light: #F43263;
+$red-normal: #E52C5A;
+$red-dark: #D22852;
+
+$border-white-light: #E3E7EC;
+$border-white-normal: #D6DAE2;
+$border-white-dark: #C6CACF;
+
+$border-gray-light: #DCE0E5;
+$border-gray-normal: #D6DAE2;
+$border-gray-dark: #C6CACF;
+
+$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-orange-light: #ED5C3D;
+$border-orange-normal: #CE5237;
+$border-orange-dark: #C14E35;
+
+$border-red-light: #E52C5A;
+$border-red-normal: #D22852;
+$border-red-dark: #CA264F;
+
+
+/*
+ * State colors:
+ */
+$gl-primary: $blue-normal;
+$gl-success: $green-normal;
+$gl-info: $blue-normal;
+$gl-warning: $orange-normal;
+$gl-danger: $red-normal;
+
+/*
+ * Commit Diff Colors
+ */
+$added: #63c363;
+$deleted: #f77;
+
+/*
+ * Fonts
+ */
+$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;
diff --git a/app/assets/stylesheets/generic/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 32e2c020e06..32e2c020e06 100644
--- a/app/assets/stylesheets/generic/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
diff --git a/app/assets/stylesheets/generic/blocks.scss b/app/assets/stylesheets/generic/blocks.scss
deleted file mode 100644
index 6ce34b5c3e8..00000000000
--- a/app/assets/stylesheets/generic/blocks.scss
+++ /dev/null
@@ -1,62 +0,0 @@
-.light-well {
- background-color: #f8fafc;
- padding: 15px;
-}
-
-.centered-light-block {
- text-align: center;
- color: $gl-gray;
- margin: 20px;
-}
-
-.nothing-here-block {
- text-align: center;
- padding: 20px;
- color: $gl-gray;
- font-weight: normal;
- font-size: 16px;
- line-height: 36px;
-}
-
-.gray-content-block {
- margin: -$gl-padding;
- background-color: $background-color;
- padding: $gl-padding;
- margin-bottom: 0px;
- border-top: 1px solid $border-color;
- border-bottom: 1px solid $border-color;
- color: $gl-gray;
-
- &.top-block {
- border-top: none;
- }
-
- &.middle-block {
- margin-top: 0;
- margin-bottom: 0;
- }
-
- &.clear-block {
- margin-bottom: $gl-padding - 1px;
- padding-bottom: $gl-padding;
- }
-
- &.second-block {
- margin-top: -1px;
- margin-bottom: 0;
- }
-
- &.footer-block {
- margin-top: 0;
- border-bottom: none;
- margin-bottom: -$gl-padding;
- }
-
- .title {
- color: $gl-text-color;
- }
-
- .oneline {
- line-height: 42px;
- }
-}
diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss
deleted file mode 100644
index 46ef595ddf0..00000000000
--- a/app/assets/stylesheets/generic/buttons.scss
+++ /dev/null
@@ -1,90 +0,0 @@
-.btn {
- @extend .btn-default;
-
- &.btn-new {
- @extend .btn-success;
- }
-
- &.btn-create {
- @extend .btn-success;
- }
-
- &.btn-save {
- @extend .btn-success;
- }
-
- &.btn-remove {
- @extend .btn-danger;
- }
-
- &.btn-cancel {
- float: right;
- }
-
- &.btn-close {
- color: $gl-danger;
- border-color: $gl-danger;
- &:hover {
- color: #B94A48;
- }
- }
-
- &.btn-reopen {
- color: $gl-success;
- border-color: $gl-success;
- &:hover {
- color: #468847;
- }
- }
-
- &.btn-grouped {
- margin-right: 7px;
- float: left;
- &:last-child {
- margin-right: 0px;
- }
- }
-
- &.btn-save {
- @extend .btn-primary;
- }
-
- &.btn-new, &.btn-create {
- @extend .btn-success;
- }
-}
-
-.btn-block {
- width: 100%;
- margin: 0;
- margin-bottom: 15px;
- &.btn {
- padding: 6px 0;
- }
-}
-
-.btn-group {
- &.btn-grouped {
- margin-right: 7px;
- float: left;
- &:last-child {
- margin-right: 0px;
- }
- }
-}
-
-.btn-group-next {
- .btn {
- padding: 9px 0px;
- font-size: 15px;
- color: #7f8fa4;
- border-color: #e7e9ed;
- width: 140px;
-
- &.active {
- border-color: $gl-info;
- background: $gl-info;
- color: #fff;
- }
- }
-}
diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss
deleted file mode 100644
index f0860de1c49..00000000000
--- a/app/assets/stylesheets/generic/selects.scss
+++ /dev/null
@@ -1,118 +0,0 @@
-/** Select2 selectbox style override **/
-.select2-container, .select2-container.select2-drop-above {
- .select2-choice {
- background: #FFF;
- border-color: #DDD;
- height: 42px;
- padding: 8px $gl-padding;
- font-size: $gl-font-size;
- line-height: 1.42857143;
-
- @include border-radius(4px);
-
- .select2-arrow {
- background: #FFF;
- border-left: none;
- padding-top: 5px;
- }
- }
-}
-
-.select2-container-multi .select2-choices {
- @include border-radius(4px);
- border-color: #CCC;
-}
-
-.select2-container-multi .select2-choices .select2-search-field input {
- padding: 8px 14px;
- font-size: 13px;
- line-height: 18px;
- height: auto;
-}
-
-.select2-drop-active {
- border: 1px solid #BBB !important;
- margin-top: 4px;
- font-size: 13px;
-
- &.select2-drop-above {
- margin-bottom: 8px;
- }
-
- .select2-search input {
- background: #fafafa;
- border-color: #DDD;
- }
-
- .select2-results {
- max-height: 350px;
- .select2-highlighted {
- background: $gl-primary;
- }
- }
-}
-
-.select2-container {
- width: 100% !important;
-}
-
-/** Branch/tag selector **/
-.project-refs-form .select2-container {
- width: 160px !important;
-}
-
-.ajax-users-dropdown, .ajax-project-users-dropdown {
- .select2-search {
- padding-top: 4px;
- }
-}
-
-.ajax-users-select {
- width: 400px;
-
- &.input-large {
- width: 210px;
- }
-
- &.input-clamp {
- max-width: 100%;
- }
-}
-
-.group-result {
- .group-image {
- float: left;
- }
- .group-name {
- font-weight: bold;
- }
- .group-path {
- color: #999;
- }
-}
-
-.user-result {
- .user-image {
- float: left;
- }
- .user-name {
- }
- .user-username {
- color: #999;
- }
-}
-
-.namespace-result {
- .namespace-kind {
- color: #AAA;
- font-weight: normal;
- }
- .namespace-path {
- margin-left: 10px;
- font-weight: bolder;
- }
-}
-
-.ajax-users-dropdown {
- min-width: 225px !important;
-}
diff --git a/app/assets/stylesheets/generic/tables.scss b/app/assets/stylesheets/generic/tables.scss
deleted file mode 100644
index a66e45577de..00000000000
--- a/app/assets/stylesheets/generic/tables.scss
+++ /dev/null
@@ -1,20 +0,0 @@
-table {
- &.table {
- tr {
- td, th {
- padding: 8px 10px;
- line-height: 20px;
- vertical-align: middle;
- }
- th {
- font-weight: normal;
- font-size: 15px;
- border-bottom: 1px solid $border-color !important;
- }
- td {
- border-color: #F1F1F1 !important;
- border-bottom: 1px solid;
- }
- }
- }
-}
diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss
deleted file mode 100644
index 551db31db12..00000000000
--- a/app/assets/stylesheets/generic/typography.scss
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * Headers
- *
- */
-.page-title {
- margin-top: 0px;
- line-height: 1.5;
- font-weight: normal;
- margin-bottom: 5px;
-}
-
-h1, h2, h3, h4, h5, h6 {
- color: $gl-header-color;
- font-weight: 500;
-}
-
-/** CODE **/
-pre {
- font-family: $monospace_font;
-
- &.dark {
- background: #333;
- color: $background-color;
- }
-
- &.plain-readme {
- background: none;
- border: none;
- padding: 0;
- margin: 0;
- font-size: 14px;
- }
-}
-
-.monospace {
- font-family: $monospace_font;
-}
-
-code {
- &.key-fingerprint {
- background: $body-bg;
- color: $text-color;
- }
-}
-
-a > code {
- color: $link-color;
-}
-
-/**
- * Wiki typography
- *
- */
-.wiki {
- @include md-typography;
-
- word-wrap: break-word;
- padding: 7px;
-
- /* Link to current header. */
- h1, h2, h3, h4, h5, h6 {
- position: relative;
-
- a.anchor {
- // Setting `display: none` would prevent the anchor being scrolled to, so
- // instead we set the height to 0 and it gets updated on hover.
- height: 0;
- }
-
- &:hover > a.anchor {
- $size: 16px;
- position: absolute;
- right: 100%;
- top: 50%;
- margin-top: -$size/2;
- margin-right: 0px;
- padding-right: 20px;
- display: inline-block;
- width: $size;
- height: $size;
- background-image: image-url("icon-link.png");
- background-size: contain;
- background-repeat: no-repeat;
- }
- }
-
- ul,ol {
- padding: 0;
- margin: 6px 0 6px 18px !important;
- }
- ol {
- color: #5c5d5e;
- }
-}
-
-.md-area {
- @include md-typography;
-}
-
-.md {
- @include md-typography;
-}
-
-/**
- * Textareas intended for GFM
- *
- */
-textarea.js-gfm-input {
- font-family: $monospace_font;
-}
-
-.md-preview {
-}
-
-.strikethrough {
- text-decoration: line-through;
-}
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 8323a8598ec..6a2b25ddc67 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -1,11 +1,10 @@
/* https://github.com/MozMorris/tomorrow-pygments */
-pre.code.highlight.dark,
.code.dark {
- background-color: #1d1f21;
- color: #c5c8c6;
+ background-color: #1d1f21 !important;
+ color: #c5c8c6 !important;
- pre.code,
+ pre.highlight,
.line-numbers,
.line-numbers a {
background-color: #1d1f21 !important;
@@ -23,8 +22,8 @@ pre.code.highlight.dark,
// Search result highlight
span.highlight_word {
- background: #ffe792;
- color: #000000;
+ background-color: #ffe792 !important;
+ color: #000000 !important;
}
.hll { background-color: #373b41 }
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index e8381674336..8560c3c490f 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -1,15 +1,14 @@
/* https://github.com/richleland/pygments-css/blob/master/monokai.css */
-pre.code.monokai,
.code.monokai {
- background: #272822;
- color: #f8f8f2;
+ background-color: #272822 !important;
+ color: #f8f8f2 !important;
pre.highlight,
.line-numbers,
.line-numbers a {
- background:#272822 !important;
- color:#f8f8f2 !important;
+ background-color :#272822 !important;
+ color: #f8f8f2 !important;
}
pre.code {
@@ -23,8 +22,8 @@ pre.code.monokai,
// Search result highlight
span.highlight_word {
- background: #ffe792;
- color: #000000;
+ background-color: #ffe792 !important;
+ color: #000000 !important;
}
.hll { background-color: #49483e }
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index bd41480aefb..7d489a9666b 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -1,11 +1,10 @@
/* https://gist.github.com/qguv/7936275 */
-pre.code.highlight.solarized-dark,
.code.solarized-dark {
- background-color: #002b36;
- color: #93a1a1;
+ background-color: #002b36 !important;
+ color: #93a1a1 !important;
- pre.code,
+ pre.highlight,
.line-numbers,
.line-numbers a {
background-color: #002b36 !important;
@@ -23,7 +22,7 @@ pre.code.highlight.solarized-dark,
// Search result highlight
span.highlight_word {
- background: #094554;
+ background-color: #094554 !important;
}
/* Solarized Dark
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 4cc62863870..200ed346446 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -1,11 +1,10 @@
/* https://gist.github.com/qguv/7936275 */
-pre.code.highlight.solarized-light,
.code.solarized-light {
- background-color: #fdf6e3;
- color: #586e75;
+ background-color: #fdf6e3 !important;
+ color: #586e75 !important;
- pre.code,
+ pre.highlight,
.line-numbers,
.line-numbers a {
background-color: #fdf6e3 !important;
@@ -23,7 +22,7 @@ pre.code.highlight.solarized-light,
// Search result highlight
span.highlight_word {
- background: #eee8d5;
+ background-color: #eee8d5 !important;
}
/* Solarized Light
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 5de589109bd..e2626da7871 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,23 +1,20 @@
/* https://github.com/aahan/pygments-github-style */
-pre.code.highlight.white,
.code.white {
- background-color: #fff;
- color: #333;
+ background-color: #f8fafc !important;
+ color: #5b6169 !important;
+ pre.highlight,
.line-numbers,
.line-numbers a {
background-color: $background-color !important;
color: $gl-gray !important;
}
- pre.highlight {
- background-color: #fff !important;
- color: #333 !important;
- }
-
pre.code {
border-left: 1px solid $border-color;
+ background-color: #fff !important;
+ color: #333 !important;
}
// highlight line via anchor
@@ -27,7 +24,7 @@ pre.code.highlight.white,
// Search result highlight
span.highlight_word {
- background: #fafe3d;
+ background-color: #fafe3d !important;
}
.hll { background-color: #f8f8f8 }
diff --git a/app/assets/stylesheets/ci/builds.scss b/app/assets/stylesheets/pages/builds.scss
index a11a935b54d..da9965f007a 100644
--- a/app/assets/stylesheets/ci/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,4 +1,4 @@
-.ci-body {
+.build-page {
pre.trace {
background: #111111;
color: #fff;
@@ -21,7 +21,7 @@
.autoscroll-container {
position: fixed;
- bottom: 10px;
+ bottom: 20px;
right: 20px;
z-index: 100;
}
@@ -34,7 +34,7 @@
a {
display: block;
- margin-bottom: 5px;
+ margin-bottom: 10px;
}
}
@@ -67,4 +67,9 @@
color: #3084bb !important;
}
}
+
+ .build-top-menu {
+ margin-top: 0;
+ margin-bottom: 2px;
+ }
}
diff --git a/app/assets/stylesheets/ci/projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
index 8c5273abcda..2a7b5cfc7fd 100644
--- a/app/assets/stylesheets/ci/projects.scss
+++ b/app/assets/stylesheets/pages/ci_projects.scss
@@ -6,11 +6,6 @@
line-height: 1.5;
}
- .wide-table-holder {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- }
-
.builds,
.projects-table {
.light {
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 051ca3792c3..17245d3be7b 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -2,10 +2,6 @@
display: block;
}
-.commit-title{
- margin-bottom: 10px;
-}
-
.commit-author, .commit-committer{
display: block;
color: #999;
@@ -41,6 +37,8 @@
.commit-box {
.commit-title {
margin: 0;
+ font-size: 23px;
+ color: #313236;
}
.commit-description {
@@ -49,30 +47,34 @@
}
.file-stats {
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 10px 0;
+
+ li {
+ padding: 3px 0px;
+ line-height: 20px;
+ }
+ }
.new-file {
a {
- color: #090;
- }
- i {
- color: #1BCF00;
+ color: $gl-text-green;
}
}
.renamed-file {
- i {
- color: #FE9300;
+ a {
+ color: $gl-text-orange;
}
}
.deleted-file {
a {
- color: #B00;
- }
- i {
- color: #EE0000;
+ color: $gl-text-red;
}
}
.edit-file{
- i{
- color: #555;
+ a {
+ color: $gl-text-color;
}
}
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index de2ae93df37..c9dfcff6290 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -1,5 +1,6 @@
.commits-compare-switch{
- @extend .btn;
+ @include btn-default;
+ @include btn-white;
background: image-url("switch_icon.png") no-repeat center center;
text-indent: -9999px;
float: left;
@@ -32,6 +33,8 @@
}
li.commit {
+ list-style: none;
+
.commit-row-title {
font-size: $list-font-size;
line-height: 20px;
@@ -112,3 +115,10 @@ li.commit {
}
}
}
+
+.branch-commit {
+ color: $gl-gray;
+ .commit-id, .commit-row-message {
+ color: $gl-gray;
+ }
+}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 5e7e59a6af8..afd6fb73675 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1,3 +1,4 @@
+// Common
.diff-file {
margin-left: -$gl-padding;
margin-right: -$gl-padding;
@@ -12,24 +13,17 @@
color: #555;
z-index: 10;
- > span {
+ .diff-title {
font-family: $monospace_font;
word-break: break-all;
- margin-right: 200px;
display: block;
.file-mode {
- margin-left: 10px;
color: #777;
}
}
- .diff-btn-group {
- float: right;
- position: absolute;
- top: 5px;
- right: 15px;
-
+ .diff-controls {
.btn {
padding: 0px 10px;
font-size: 13px;
@@ -90,12 +84,12 @@
}
}
- tr.line_holder.parallel{
+ tr.line_holder.parallel {
.old_line, .new_line, .diff_line {
min-width: 50px;
}
- td.line_content.parallel{
+ td.line_content.parallel {
width: 50%;
}
}
@@ -105,7 +99,7 @@
padding: 0px;
border: none;
background: $background-color;
- color: rgba(0,0,0,0.3);
+ color: rgba(0, 0, 0, 0.3);
padding: 0px 5px;
border-right: 1px solid $border-color;
text-align: right;
@@ -117,7 +111,7 @@
float: left;
width: 35px;
font-weight: normal;
- color: rgba(0,0,0,0.3);
+ color: rgba(0, 0, 0, 0.3);
&:hover {
text-decoration: underline;
}
@@ -168,7 +162,7 @@
background: #ddd;
text-align: center;
padding: 30px;
- .wrap{
+ .wrap {
display: inline-block;
}
@@ -176,7 +170,7 @@
display: inline-block;
background-color: #fff;
line-height: 0;
- img{
+ img {
border: 1px solid #FFF;
background: image-url('trans_bg.gif');
max-width: 100%;
@@ -189,21 +183,21 @@
border: 1px solid $added;
}
}
- .image-info{
+ .image-info {
font-size: 12px;
margin: 5px 0 0 0;
color: grey;
}
- .view.swipe{
+ .view.swipe {
position: relative;
- .swipe-frame{
+ .swipe-frame {
display: block;
margin: auto;
position: relative;
}
- .swipe-wrap{
+ .swipe-wrap {
overflow: hidden;
border-left: 1px solid #999;
position: absolute;
@@ -211,33 +205,33 @@
top: 13px;
right: 7px;
}
- .frame{
+ .frame {
top: 0;
right: 0;
position: absolute;
- &.deleted{
+ &.deleted {
margin: 0;
display: block;
top: 13px;
right: 7px;
}
}
- .swipe-bar{
+ .swipe-bar {
display: block;
height: 100%;
width: 15px;
z-index: 100;
position: absolute;
cursor: pointer;
- &:hover{
- .top-handle{
+ &:hover {
+ .top-handle {
background-position: -15px 3px;
}
- .bottom-handle{
+ .bottom-handle {
background-position: -15px -11px;
}
- };
- .top-handle{
+ }
+ .top-handle {
display: block;
height: 14px;
width: 15px;
@@ -245,7 +239,7 @@
top: 0px;
background: image-url('swipemode_sprites.gif') 0 3px no-repeat;
}
- .bottom-handle{
+ .bottom-handle {
display: block;
height: 14px;
width: 15px;
@@ -254,9 +248,10 @@
background: image-url('swipemode_sprites.gif') 0 -11px no-repeat;
}
}
- } //.view.swipe
- .view.onion-skin{
- .onion-skin-frame{
+ }
+ //.view.swipe
+ .view.onion-skin {
+ .onion-skin-frame {
display: block;
margin: auto;
position: relative;
@@ -267,7 +262,7 @@
top: 0px;
left: 0px;
}
- .controls{
+ .controls {
display: block;
height: 14px;
width: 300px;
@@ -277,7 +272,7 @@
left: 50%;
margin-left: -150px;
- .drag-track{
+ .drag-track {
display: block;
position: absolute;
left: 12px;
@@ -317,39 +312,40 @@
background: image-url('onion_skin_sprites.gif') -2px -10px no-repeat;
}
}
- } //.view.onion-skin
+ }
+ //.view.onion-skin
}
- .view-modes{
+ .view-modes {
padding: 10px;
text-align: center;
background: #EEE;
- ul, li{
+ ul, li {
list-style: none;
margin: 0;
padding: 0;
display: inline-block;
}
- li{
+ li {
color: grey;
border-left: 1px solid #c1c1c1;
padding: 0 12px 0 16px;
cursor: pointer;
- &:first-child{
+ &:first-child {
border-left: none;
}
- &:hover{
+ &:hover {
text-decoration: underline;
}
- &.active{
- &:hover{
+ &.active {
+ &:hover {
text-decoration: none;
}
cursor: default;
color: #333;
}
- &.disabled{
+ &.disabled {
display: none;
}
}
@@ -371,5 +367,38 @@
.inline-parallel-buttons {
float: right;
- margin-top: -5px;
+}
+
+// Mobile
+@media (max-width: 480px) {
+ .diff-title {
+ margin: 0;
+
+ .file-mode {
+ display: none;
+ }
+ }
+
+ .diff-controls {
+ position: static;
+ text-align: center;
+ }
+}
+
+// Bigger screens
+@media (min-width: 481px) {
+ .diff-title {
+ margin-right: 200px;
+
+ .file-mode {
+ margin-left: 10px;
+ }
+ }
+
+ .diff-controls {
+ float: right;
+ position: absolute;
+ top: 5px;
+ right: 15px;
+ }
}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 1d565477dd4..39d916cd336 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -19,48 +19,38 @@
color: #B94A48;
}
}
- .commit-button-annotation {
- display: inline-block;
- margin: 0;
- padding: 2px;
-
- > * {
- float: left;
- }
-
- .message {
- display: inline-block;
- margin: 5px 8px 0 8px;
- }
- }
.file-title {
@extend .monospace;
+
+ line-height: 42px;
+ padding-top: 7px;
+ padding-bottom: 7px;
}
.editor-ref {
background: $background-color;
- padding: 11px 15px;
+ padding-right: $gl-padding;
border-right: 1px solid $border-color;
- display: inline-block;
- margin: -5px -5px;
+ display: block;
+ float: left;
margin-right: 10px;
}
.editor-file-name {
- .new-file-name {
- display: inline-block;
- width: 200px;
- }
+ @extend .monospace;
+
+ float: left;
+ margin-right: 10px;
+ }
- .form-control {
- margin-top: -3px;
- }
+ .new-file-name {
+ display: inline-block;
+ width: 450px;
+ float: left;
}
- .form-actions {
- margin: -$gl-padding;
- margin-top: 0;
- padding: $gl-padding
+ .select2 {
+ float: right;
}
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index ca2ee455423..282aaf2219b 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -4,10 +4,10 @@
*/
.event-item {
font-size: $gl-font-size;
- padding: $gl-padding;
+ padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px);
margin-left: -$gl-padding;
margin-right: -$gl-padding;
- border-bottom: 1px solid #f1f2f4;
+ border-bottom: 1px solid $table-border-color;
color: #7f8fa4;
&.event-inline {
@@ -16,10 +16,7 @@
top: -2px;
}
- .event-title {
- line-height: 44px;
- }
-
+ .event-title,
.event-item-timestamp {
line-height: 44px;
}
@@ -30,7 +27,7 @@
}
.avatar {
- margin-right: 15px;
+ margin-left: -($gl-avatar-size + 15px);
}
.event-title {
@@ -43,8 +40,7 @@
}
.event-body {
- margin-left: 63px;
- margin-right: 80px;
+ margin-right: 174px;
.event-note {
margin-top: 5px;
@@ -155,6 +151,8 @@
@media (max-width: $screen-xs-max) {
.event-item {
+ padding-left: $gl-padding;
+
.event-title {
white-space: normal;
overflow: visible;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 2b1b747139a..263993f59a5 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -1,8 +1,3 @@
-.new-group-member-holder {
- margin-top: 50px;
- padding-top: 20px;
-}
-
.member-search-form {
float: left;
}
@@ -10,3 +5,9 @@
.milestone-row {
@include str-truncated(90%);
}
+
+.dashboard .side .panel .panel-heading .input-group {
+ .form-control {
+ height: 42px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index 6da7a2511a2..bd224705f04 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -68,3 +68,7 @@ body.modal-open {
.modal .modal-dialog {
width: 860px;
}
+
+.documentation {
+ padding: 7px;
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index b5c61f7f91d..957da5c182e 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -51,24 +51,26 @@
.issuable-details {
.page-title {
- margin-top: -15px;
- padding: 10px 0;
+ margin-top: -$gl-padding;
+ padding: 7px 0;
margin-bottom: 0;
- color: $gl-gray;
+ color: #5c5d5e;
font-size: 16px;
+ line-height: 42px;
.author {
- color: $gl-gray;
+ color: #5c5d5e;
}
.issue-id {
- font-size: 19px;
- color: $gl-text-color;
+ color: #5c5d5e;
}
}
.issue-title {
margin: 0;
+ font-size: 23px;
+ color: #313236;
}
.description {
@@ -79,3 +81,104 @@
}
}
}
+
+.issuable-filter-count {
+ span {
+ display: block;
+ margin-bottom: -16px;
+ padding: 13px 0;
+ }
+}
+
+.issuable-show-labels {
+ a {
+ margin-right: 5px;
+ margin-bottom: 5px;
+ display: inline-block;
+ .color-label {
+ padding: 6px 10px;
+ }
+ }
+}
+
+.cross-project-reference {
+ text-align: center;
+ width: 100%;
+
+ .slead {
+ padding: 5px;
+ }
+
+ span, button {
+ background-color: $background-color;
+ }
+}
+
+.awards {
+ @include clearfix;
+ line-height: 34px;
+ margin: 2px 0;
+
+ .award {
+ @include border-radius(5px);
+
+ border: 1px solid;
+ padding: 0px 10px;
+ float: left;
+ margin: 0 5px;
+ border-color: $border-color;
+ cursor: pointer;
+
+ &.active {
+ border-color: $border-gray-light;
+ background-color: $gray-light;
+
+ .counter {
+ font-weight: bold;
+ }
+ }
+
+ .icon {
+ float: left;
+ margin-right: 10px;
+ }
+
+ .counter {
+ float: left;
+ }
+ }
+
+ .awards-controls {
+ margin-left: 10px;
+ float: left;
+
+ .add-award {
+ font-size: 24px;
+ color: $gl-gray;
+ position: relative;
+ top: 2px;
+
+ &:hover,
+ &:link {
+ text-decoration: none;
+ }
+ }
+
+ .awards-menu {
+ padding: $gl-padding;
+ min-width: 214px;
+
+ > li {
+ cursor: pointer;
+ margin: 5px;
+ }
+ }
+ }
+
+ .awards-menu{
+ li {
+ float: left;
+ margin: 3px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 4bf58cb4a59..f5548c5b88b 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -56,17 +56,6 @@
}
}
-.issue-show-labels {
- a {
- margin-right: 5px;
- margin-bottom: 5px;
- display: inline-block;
- .color-label {
- padding: 6px 10px;
- }
- }
-}
-
form.edit-issue {
margin: 0;
}
@@ -132,6 +121,11 @@ form.edit-issue {
}
}
+.issue-closed-by-widget {
+ padding: 16px 0;
+ margin: 0px;
+}
+
.issue-form .select2-container {
width: 250px !important;
}
diff --git a/app/assets/stylesheets/ci/lint.scss b/app/assets/stylesheets/pages/lint.scss
index 6d2bd33b28b..6d2bd33b28b 100644
--- a/app/assets/stylesheets/ci/lint.scss
+++ b/app/assets/stylesheets/pages/lint.scss
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 83b866c3a64..f9c6f1b39f9 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -19,6 +19,7 @@
h1:first-child {
font-weight: normal;
margin-bottom: 30px;
+ margin-top: 0;
}
img {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index d8c8e5ad0a4..f21ad694d06 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,12 +3,10 @@
*
*/
.mr-state-widget {
- background: #f8fafc;
- margin-bottom: 20px;
+ background: #F7F8FA;
color: $gl-gray;
- border: 1px solid #eef0f2;
- @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.05));
- @include border-radius(3px);
+ border: 1px solid #dce0e6;
+ @include border-radius(2px);
form {
margin-bottom: 0;
@@ -20,6 +18,20 @@
.accept-merge-holder {
.accept-action {
display: inline-block;
+
+ .accept_merge_request {
+ &.ci-pending,
+ &.ci-running {
+ @include btn-orange;
+ }
+
+ &.ci-skipped,
+ &.ci-failed,
+ &.ci-canceled,
+ &.ci-error {
+ @include btn-red;
+ }
+ }
}
.accept-control {
@@ -74,13 +86,19 @@
.mr-widget-body,
.ci_widget,
.mr-widget-footer {
- padding: 15px;
+ padding: $gl-padding;
+ }
+
+ .normal {
+ color: #5c5d5e;
}
.mr-widget-body {
h4 {
- font-weight: bold;
+ font-weight: 600;
+ font-size: 17px;
margin: 5px 0;
+ color: #313236;
}
p:last-child {
@@ -97,14 +115,8 @@
}
}
-.merge-request .merge-request-tabs{
- @include nav-menu;
- margin: -$gl-padding;
- padding: $gl-padding;
- text-align: center;
- border-top: 1px solid #e7e9ed;
- margin-top: 18px;
- margin-bottom: 3px;
+.merge-request-details {
+ margin-bottom: $gl-padding;
}
.mr_source_commit,
@@ -120,10 +132,12 @@
}
.label-branch {
- color: #222;
+ color: #313236;
font-family: $monospace_font;
font-weight: bold;
overflow: hidden;
+ font-size: 14px;
+ margin: 0 3px;
}
.mr-list {
@@ -159,33 +173,27 @@
line-height: 1.1;
}
-.merge-request-form-info {
- padding-top: 15px;
-}
-
// hide mr close link for inline diff comment form
.diff-file .close-mr-link,
.diff-file .reopen-mr-link {
display: none;
}
-.merge-request-show-labels {
- a {
- margin-right: 5px;
- margin-bottom: 5px;
- display: inline-block;
- .color-label {
- padding: 6px 10px;
- }
- }
-}
-
.merge-request-form .select2-container {
width: 250px !important;
}
#modal_merge_info .modal-dialog {
width: 600px;
+
+ .btn-clipboard {
+ @extend .pull-right;
+
+ margin-right: 18px;
+ margin-top: 5px;
+ position: absolute;
+ right: 0;
+ }
}
.mr-source-target {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index fdc2c3332df..e1a72af0013 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -7,6 +7,7 @@
}
.reply-btn {
@extend .btn-primary;
+ margin: 10px $gl-padding;
}
.diff-file .diff-content {
tr.line_holder:hover {
@@ -38,9 +39,8 @@
}
.new_note, .edit_note {
- .buttons {
- margin-top: 8px;
- margin-bottom: 3px;
+ .note-form-actions {
+ margin-top: $gl-padding;
}
.note-preview-holder {
@@ -56,6 +56,10 @@
.note_text {
width: 100%;
}
+
+ .comment-hints {
+ margin-top: -12px;
+ }
}
/* loading indicator */
@@ -65,19 +69,18 @@
.note-image-attach {
@extend .col-md-4;
- @extend .thumbnail;
margin-left: 45px;
float: none;
}
.common-note-form {
margin: 0;
- background: #f8fafc;
+ background: #F7F8FA;
padding: $gl-padding;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
- border-right: 1px solid #f1f2f4;
- border-top: 1px solid #f1f2f4;
+ border-right: 1px solid $border-color;
+ border-top: 1px solid $border-color;
margin-bottom: -$gl-padding;
}
@@ -147,7 +150,6 @@
.discussion-reply-holder {
background: $background-color;
- padding: 10px 15px;
border-top: 1px solid $border-color;
}
}
@@ -168,8 +170,8 @@
.comment-hints {
color: #999;
background: #FFF;
- padding: 5px;
- margin-top: -11px;
+ padding: 7px;
+ margin-top: -7px;
border: 1px solid $border-color;
font-size: 13px;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 2a77f065aed..1980fe0d458 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -18,7 +18,7 @@ ul.notes {
font-size: 14px;
padding-top: 10px;
padding-bottom: 10px;
- background: #f8fafc;
+ background: #FDFDFD;
.timeline-icon {
.avatar {
@@ -30,7 +30,6 @@ ul.notes {
.discussion-header,
.note-header {
@extend .cgray;
- padding-bottom: 15px;
a:hover {
text-decoration: none;
@@ -75,6 +74,10 @@ ul.notes {
}
}
+ .discussion-body {
+ padding-top: 15px;
+ }
+
.discussion {
overflow: hidden;
display: block;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 8e4f0eb2b25..1d6ca0dfc13 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -47,3 +47,31 @@
}
}
}
+
+.calendar-hint {
+ margin-top: -12px;
+ float: right;
+ font-size: 12px;
+}
+
+.profile-link-holder {
+ display: inline;
+
+ &:after {
+ content: "\00B7";
+ padding: 0px 6px;
+ font-weight: bold;
+ }
+
+ &:last-child {
+ &:after {
+ content: "";
+ padding: 0;
+ }
+ }
+
+ a {
+ color: $blue-dark;
+ text-decoration: none;
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 8d75f3aad49..2ded32dba12 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -1,8 +1,19 @@
+.alert_holder {
+ margin: -16px;
+
+ .alert-link {
+ font-weight: normal;
+ }
+}
+.no-ssh-key-message, .project-limit-message {
+ background-color: #f28d35;
+ margin-bottom: 16px;
+}
.new_project,
.edit_project {
fieldset.features {
.control-label {
- font-weight: bold;
+ font-weight: normal;
}
}
}
@@ -15,14 +26,8 @@
}
.project-home-panel {
- text-align: center;
- background: #f7f8fa;
- margin: -$gl-padding;
- padding: $gl-padding;
- padding-top: 40px;
-
.project-identicon-holder {
- margin-bottom: 15px;
+ margin-bottom: 16px;
.avatar, .identicon {
margin: 0 auto;
@@ -35,61 +40,86 @@
}
.project-home-dropdown {
- margin: 11px 3px 0;
+ margin: 13px 0px 0;
+ }
+
+ .notifications-btn {
+ .fa-bell {
+ margin-right: 6px;
+ }
+
+ .fa-angle-down {
+ margin-left: 6px;
+ }
}
.project-home-desc {
h1 {
+ color: #313236;
margin: 0;
- margin-bottom: 10px;
+ margin-bottom: 6px;
font-size: 23px;
font-weight: normal;
}
p {
- color: #7f8fa4;
+ padding: 0 $gl-padding;
+ color: #5c5d5e;
}
}
.git-clone-holder {
- max-width: 600px;
- margin: 20px auto;
+ 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;
}
}
+ .input-group {
+ display: inline-table;
+ position: relative;
+ top: 17px;
+ }
+
.project-repo-buttons {
- margin-top: $gl-padding;
- margin-bottom: 25px;
+ margin-top: 12px;
+ margin-bottom: 0px;
.btn {
- @extend .btn-info;
-
- text-transform: uppercase;
- font-size: 15px;
- line-height: 20px;
- padding: 8px 14px;
- border-radius: 3px;
- margin-left: 10px;
+ @include btn-gray;
.count {
- padding-left: 7px;
display: inline-block;
- margin-left: 7px;
}
}
}
}
+.split-one {
+ display: inline-table;
+ margin-right: 12px;
+
+ a {
+ margin: -1px !important;
+ }
+}
+
.git-clone-holder {
.project-home-dropdown + & {
margin-right: 45px;
@@ -99,11 +129,11 @@
cursor: auto;
@extend .monospace;
background: #FAFAFA;
- width: 100%;
+ width: 101%;
}
.input-group-addon {
- background: #FAFAFA;
+ background: #f7f8fa;
&.git-protocols {
padding: 0;
@@ -111,28 +141,131 @@
.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;
i {
- margin: 0 3px;
+ margin: 2px 0;
font-size: 20px;
}
.option-title {
- font-weight: bold;
+ font-weight: normal;
display: inline-block;
+ color: #313236;
}
.option-descr {
- margin-left: 36px;
- color: $gray;
+ margin-left: 29px;
+ color: #54565b;
}
}
}
@@ -232,15 +365,28 @@ table.table.protected-branches-list tr.no-border {
.project-stats {
text-align: center;
- margin-top: 0;
+ margin-top: $gl-padding;
margin-bottom: 0;
- padding-top: 5px;
- padding-bottom: 0;
+ padding-top: 10px;
+ padding-bottom: 4px;
ul.nav-pills {
display:inline-block;
}
+ .nav-pills li {
+ display:inline;
+ }
+
+ .nav > li > a {
+ @include btn-default;
+ @include btn-gray;
+
+ background-color: transparent;
+ border: 1px solid #f7f8fa;
+ margin-left: 12px;
+ }
+
li {
display:inline;
}
@@ -251,11 +397,11 @@ table.table.protected-branches-list tr.no-border {
}
li.missing a {
- color: #bbb;
- border: 1px dashed #ccc;
+ color: #5a6069;
+ border: 1px dashed #dce0e5;
&:hover {
- background-color: #FAFAFA;
+ background-color: #f0f2f5;
}
}
}
@@ -273,15 +419,43 @@ pre.light-well {
border-bottom: 1px solid #e7e9ed;
}
+.git-empty {
+ margin: 0 7px 0 7px;
+
+ h5 {
+ color: #5c5d5e;
+ }
+
+ .light-well {
+ @include border-radius (2px);
+
+ color: #5b6169;
+ font-size: 13px;
+ line-height: 1.6em;
+ }
+}
+
+.project-footer {
+ margin-top: 20px;
+
+ .btn-remove {
+ @include btn-middle;
+ @include btn-red;
+
+ float: left !important;
+ }
+}
+
/*
* Projects list rendered on dashboard and user page
*/
+
.projects-list {
@include basic-list;
.project-row {
padding: $gl-padding;
- border-color: #f1f2f4;
+ border-color: $table-border-color;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
@@ -336,6 +510,45 @@ pre.light-well {
}
}
-.inline-form {
- display: inline-block;
+.project-last-commit {
+ margin: 0 7px;
+
+ .ci-status {
+ margin-right: 16px;
+ }
+
+ .commit-row-message {
+ color: $gl-gray;
+ }
+
+ .commit_short_id {
+ margin-right: 5px;
+ color: $gl-link-color;
+ font-weight: 600;
+ }
+
+ .commit-author-link {
+ margin-left: 7px;
+ text-decoration: none;
+ .avatar {
+ float: none;
+ margin-right: 4px;
+ }
+
+ .commit-author-name {
+ font-weight: 600;
+ }
+ }
+}
+
+.project-show-readme .readme-holder {
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ padding: ($gl-padding + 7px);
+ border-top: 0;
+
+ .edit-project-readme {
+ z-index: 100;
+ position: relative;
+ }
}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
new file mode 100644
index 00000000000..a9111a7388f
--- /dev/null
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -0,0 +1,34 @@
+.runner-state {
+ padding: 6px 12px;
+ margin-right: 10px;
+ color: #FFF;
+
+ &.runner-state-shared {
+ background: #32b186;
+ }
+ &.runner-state-specific {
+ background: #3498db;
+ }
+}
+
+.runner-status-online {
+ color: green;
+}
+
+.runner-status-offline {
+ color: gray;
+}
+
+.runner-status-paused {
+ color: red;
+}
+
+.runner {
+ .btn {
+ padding: 1px 6px;
+ }
+
+ h4 {
+ font-weight: normal;
+ }
+}
diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss
new file mode 100644
index 00000000000..92d84d9640f
--- /dev/null
+++ b/app/assets/stylesheets/pages/sherlock.scss
@@ -0,0 +1,33 @@
+table .sherlock-code {
+ max-width: 700px;
+}
+
+.sherlock-code {
+ pre {
+ word-wrap: normal;
+ }
+
+ pre code {
+ white-space: pre;
+ }
+}
+
+.sherlock-line-samples-table {
+ margin-bottom: 0px !important;
+
+ thead tr th,
+ tbody tr td {
+ font-size: 13px !important;
+ text-align: right;
+ padding: 0px 10px !important;
+ }
+}
+
+.sherlock-file-sample pre {
+ padding-top: 28px !important;
+}
+
+.sherlock-line-samples-table .slow {
+ color: $red-light;
+ font-weight: bold;
+}
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index a3d7aba054d..bb74e50151d 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -1,8 +1,3 @@
-.my-snippets li:first-child {
- h4 { margin-top: 0; }
- padding-top: 0;
-}
-
.snippet-form-holder .file-holder .file-title {
padding: 2px;
}
@@ -30,3 +25,24 @@
}
}
}
+
+.snippet-holder {
+ margin-bottom: -$gl-padding;
+
+ .file-holder {
+ border-top: 0;
+ }
+}
+
+.snippet-box {
+ @include border-radius(2px);
+
+ display: block;
+ float: left;
+ padding: 0 $gl-padding;
+ font-weight: normal;
+ margin-right: 10px;
+ font-size: $gl-font-size;
+ border: 1px solid;
+ line-height: 40px;
+}
diff --git a/app/assets/stylesheets/ci/status.scss b/app/assets/stylesheets/pages/status.scss
index a7d3b2197f1..a7d3b2197f1 100644
--- a/app/assets/stylesheets/ci/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 271cc547e2b..d4ab6967ccd 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -1,22 +1,13 @@
.tree-holder {
- .tree-content-holder {
- float: left;
- width: 100%;
- }
-
- .tree_progress {
- display: none;
- margin: 20px;
- &.loading {
- display: block;
- }
- }
.tree-table {
- @extend .table;
- @include border-radius(0);
+ margin-bottom: 0;
tr {
+ > td, > th {
+ line-height: 28px;
+ }
+
&:hover {
td {
background: $hover;
@@ -27,9 +18,9 @@
}
&.selected {
td {
- background: $background-color;
- border-top: 1px solid #EEE;
- border-bottom: 1px solid #EEE;
+ background: $gray-dark;
+ border-top: 1px solid $border-gray-dark;
+ border-bottom: 1px solid $border-gray-dark;
}
}
}
@@ -85,19 +76,6 @@
margin-right: 15px;
}
-.readme-holder {
- margin: 0 auto;
-
- .readme-file-title {
- font-size: 14px;
- font-weight: bold;
- margin-bottom: 20px;
- color: #777;
- border-bottom: 1px solid #DDD;
- padding: 10px 0;
- }
-}
-
.blob-commit-info {
list-style: none;
margin: 0;
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index dfaeba41cf6..cdf514197cb 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -4,3 +4,8 @@
margin-right: auto;
padding-right: 7px;
}
+
+.wiki-last-edit-by {
+ font-size: 80%;
+ font-weight: normal;
+}
diff --git a/app/assets/stylesheets/ci/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 532dede0b23..9a50096c0d0 100644
--- a/app/assets/stylesheets/ci/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -1,4 +1,4 @@
-.ci-body {
+.build-page {
// color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg
// see also: https://gist.github.com/jasonm23/2868981
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index 65dbd5ef551..20bc5173f1d 100644
--- a/app/controllers/abuse_reports_controller.rb
+++ b/app/controllers/abuse_reports_controller.rb
@@ -9,6 +9,10 @@ class AbuseReportsController < ApplicationController
@abuse_report.reporter = current_user
if @abuse_report.save
+ if current_application_settings.admin_notification_email.present?
+ AbuseReportMailer.notify(@abuse_report.id).deliver_later
+ end
+
message = "Thank you for your report. A GitLab administrator will look into it shortly."
redirect_to root_path, notice: message
else
diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb
index 56e24386463..9083bfb41cf 100644
--- a/app/controllers/admin/application_controller.rb
+++ b/app/controllers/admin/application_controller.rb
@@ -8,4 +8,10 @@ class Admin::ApplicationController < ApplicationController
def authenticate_admin!
return render_404 unless current_user.is_admin?
end
+
+ def authorize_impersonator!
+ if session[:impersonator_id]
+ User.find_by!(username: session[:impersonator_id]).admin?
+ end
+ end
end
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 5f70582cbb7..a9bcfc7456a 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -55,8 +55,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:default_snippet_visibility,
:restricted_signup_domains_raw,
:version_check_enabled,
+ :admin_notification_email,
:user_oauth_applications,
- :ci_enabled,
+ :shared_runners_enabled,
+ :max_artifacts_size,
restricted_visibility_levels: [],
import_sources: []
)
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 0808024fc39..497c34f8f49 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -19,7 +19,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
BroadcastMessage.find(params[:id]).destroy
respond_to do |format|
- format.html { redirect_to :back }
+ format.html { redirect_back_or_default(default: { action: 'index' }) }
format.js { render nothing: true }
end
end
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index d670386f8c6..0bd19c49d8f 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -35,7 +35,7 @@ class Admin::HooksController < Admin::ApplicationController
}
@hook.execute(data, 'system_hooks')
- redirect_to :back
+ redirect_back_or_default
end
def hook_params
diff --git a/app/controllers/admin/impersonation_controller.rb b/app/controllers/admin/impersonation_controller.rb
new file mode 100644
index 00000000000..bf98af78615
--- /dev/null
+++ b/app/controllers/admin/impersonation_controller.rb
@@ -0,0 +1,38 @@
+class Admin::ImpersonationController < Admin::ApplicationController
+ skip_before_action :authenticate_admin!, only: :destroy
+
+ before_action :user
+ before_action :authorize_impersonator!
+
+ def create
+ if @user.blocked?
+ flash[:alert] = "You cannot impersonate a blocked user"
+
+ redirect_to admin_user_path(@user)
+ else
+ session[:impersonator_id] = current_user.username
+ session[:impersonator_return_to] = admin_user_path(@user)
+
+ warden.set_user(user, scope: 'user')
+
+ flash[:alert] = "You are impersonating #{user.username}."
+
+ redirect_to root_path
+ end
+ end
+
+ def destroy
+ redirect = session[:impersonator_return_to]
+
+ warden.set_user(user, scope: 'user')
+
+ session[:impersonator_return_to] = nil
+ session[:impersonator_id] = nil
+
+ redirect_to redirect || root_path
+ end
+
+ def user
+ @user ||= User.find_by!(username: params[:id] || session[:impersonator_id])
+ end
+end
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index a62170662e1..46133588332 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -39,7 +39,13 @@ class Admin::ServicesController < Admin::ApplicationController
end
def application_services_params
- params.permit(:id,
+ application_services_params = params.permit(:id,
service: Projects::ServicesController::ALLOWED_PARAMS)
+ if application_services_params[:service].is_a?(Hash)
+ Projects::ServicesController::FILTER_BLANK_PARAMS.each do |param|
+ application_services_params[:service].delete(param) if application_services_params[:service][param].blank?
+ end
+ end
+ application_services_params
end
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index a19b1abee27..d7c927d444c 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -33,33 +33,33 @@ class Admin::UsersController < Admin::ApplicationController
def block
if user.block
- redirect_to :back, notice: "Successfully blocked"
+ redirect_back_or_admin_user(notice: "Successfully blocked")
else
- redirect_to :back, alert: "Error occurred. User was not blocked"
+ redirect_back_or_admin_user(alert: "Error occurred. User was not blocked")
end
end
def unblock
if user.activate
- redirect_to :back, notice: "Successfully unblocked"
+ redirect_back_or_admin_user(notice: "Successfully unblocked")
else
- redirect_to :back, alert: "Error occurred. User was not unblocked"
+ redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked")
end
end
def unlock
if user.unlock_access!
- redirect_to :back, alert: "Successfully unlocked"
+ redirect_back_or_admin_user(alert: "Successfully unlocked")
else
- redirect_to :back, alert: "Error occurred. User was not unlocked"
+ redirect_back_or_admin_user(alert: "Error occurred. User was not unlocked")
end
end
def confirm
if user.confirm
- redirect_to :back, notice: "Successfully confirmed"
+ redirect_back_or_admin_user(notice: "Successfully confirmed")
else
- redirect_to :back, alert: "Error occurred. User was not confirmed"
+ redirect_back_or_admin_user(alert: "Error occurred. User was not confirmed")
end
end
@@ -132,7 +132,7 @@ class Admin::UsersController < Admin::ApplicationController
user.update_secondary_emails!
respond_to do |format|
- format.html { redirect_to :back, notice: "Successfully removed email." }
+ format.html { redirect_back_or_admin_user(notice: "Successfully removed email.") }
format.js { render nothing: true }
end
end
@@ -151,4 +151,12 @@ class Admin::UsersController < Admin::ApplicationController
:projects_limit, :can_create_group, :admin, :key_id
)
end
+
+ def redirect_back_or_admin_user(options = {})
+ redirect_back_or_default(default: default_route, options: options)
+ end
+
+ def default_route
+ [:admin, @user]
+ end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 9b6472a7b13..0d182e8eb04 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -30,7 +30,11 @@ class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound do |exception|
log_exception(exception)
- render "errors/not_found", layout: "errors", status: 404
+ render_404
+ end
+
+ def redirect_back_or_default(default: root_path, options: {})
+ redirect_to request.referer.present? ? :back : default, options
end
protected
@@ -55,13 +59,8 @@ class ApplicationController < ActionController::Base
end
def authenticate_user!(*args)
- # If user is not signed-in and tries to access root_path - redirect him to landing page
- # Don't redirect to the default URL to prevent endless redirections
- if current_application_settings.home_page_url.present? &&
- current_application_settings.home_page_url.chomp('/') != Gitlab.config.gitlab['url'].chomp('/')
- if current_user.nil? && root_path == request.path
- redirect_to current_application_settings.home_page_url and return
- end
+ if redirect_to_home_page_url?
+ redirect_to current_application_settings.home_page_url and return
end
super(*args)
@@ -117,9 +116,13 @@ class ApplicationController < ActionController::Base
redirect_to request.original_url.gsub(/\.git\Z/, '') and return
end
- @project = Project.find_with_namespace("#{namespace}/#{id}")
+ project_path = "#{namespace}/#{id}"
+ @project = Project.find_with_namespace(project_path)
if @project and can?(current_user, :read_project, @project)
+ if @project.path_with_namespace != project_path
+ redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) and return
+ end
@project
elsif current_user.nil?
@project = nil
@@ -144,12 +147,8 @@ class ApplicationController < ActionController::Base
render "errors/access_denied", layout: "errors", status: 404
end
- def not_found!
- render "errors/not_found", layout: "errors", status: 404
- end
-
def git_not_found!
- render "errors/git_not_found", layout: "errors", status: 404
+ render html: "errors/git_not_found", layout: "errors", status: 404
end
def method_missing(method_sym, *arguments, &block)
@@ -342,4 +341,17 @@ class ApplicationController < ActionController::Base
def git_import_enabled?
current_application_settings.import_sources.include?('git')
end
+
+ def redirect_to_home_page_url?
+ # If user is not signed-in and tries to access root_path - redirect him to landing page
+ # Don't redirect to the default URL to prevent endless redirections
+ return false unless current_application_settings.home_page_url.present?
+
+ home_page_url = current_application_settings.home_page_url.chomp('/')
+ root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')]
+
+ return false if root_urls.include?(home_page_url)
+
+ current_user.nil? && root_path == request.path
+ end
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 202e9da9eee..77c8dafc012 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -1,41 +1,15 @@
class AutocompleteController < ApplicationController
skip_before_action :authenticate_user!, only: [:users]
+ before_action :find_users, only: [:users]
def users
- begin
- @users =
- if params[:project_id].present?
- project = Project.find(params[:project_id])
-
- if can?(current_user, :read_project, project)
- project.team.users
- end
- elsif params[:group_id]
- group = Group.find(params[:group_id])
-
- if can?(current_user, :read_group, group)
- group.users
- end
- elsif current_user
- User.all
- end
- rescue ActiveRecord::RecordNotFound
- if current_user
- return render json: {}, status: 404
- end
- end
-
- if @users.nil? && current_user.nil?
- authenticate_user!
- end
-
@users ||= User.none
@users = @users.search(params[:search]) if params[:search].present?
@users = @users.active
@users = @users.reorder(:name)
@users = @users.page(params[:page]).per(PER_PAGE)
- unless params[:search].present?
+ if params[:search].blank?
# Include current user if available to filter by "Me"
if params[:current_user] && current_user
@users = [*@users, current_user].uniq
@@ -49,4 +23,25 @@ class AutocompleteController < ApplicationController
@user = User.find(params[:id])
render json: @user, only: [:name, :username, :id], methods: [:avatar_url]
end
+
+ private
+
+ def find_users
+ @users =
+ if params[:project_id].present?
+ project = Project.find(params[:project_id])
+ return render_404 unless can?(current_user, :read_project, project)
+
+ project.team.users
+ elsif params[:group_id].present?
+ group = Group.find(params[:group_id])
+ return render_404 unless can?(current_user, :read_group, group)
+
+ group.users
+ elsif current_user
+ User.all
+ else
+ User.none
+ end
+ end
end
diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb
index dc3508b49dd..0cafad27418 100644
--- a/app/controllers/ci/admin/runners_controller.rb
+++ b/app/controllers/ci/admin/runners_controller.rb
@@ -6,14 +6,18 @@ module Ci
@runners = Ci::Runner.order('id DESC')
@runners = @runners.search(params[:search]) if params[:search].present?
@runners = @runners.page(params[:page]).per(30)
- @active_runners_cnt = Ci::Runner.where("contacted_at > ?", 1.minutes.ago).count
+ @active_runners_cnt = Ci::Runner.online.count
end
def show
@builds = @runner.builds.order('id DESC').first(30)
@projects = Ci::Project.all
- @projects = @projects.search(params[:search]) if params[:search].present?
+ if params[:search].present?
+ @gl_projects = ::Project.search(params[:search])
+ @projects = @projects.where(gitlab_id: @gl_projects.select(:id))
+ end
@projects = @projects.where("ci_projects.id NOT IN (?)", @runner.projects.pluck(:id)) if @runner.projects.any?
+ @projects = @projects.joins(:gl_project)
@projects = @projects.page(params[:page]).per(30)
end
@@ -63,7 +67,7 @@ module Ci
end
def runner_params
- params.require(:runner).permit(:token, :description, :tag_list, :contacted_at, :active)
+ params.require(:runner).permit(:token, :description, :tag_list, :active)
end
end
end
diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb
index d8227e632e4..848f2b4e314 100644
--- a/app/controllers/ci/application_controller.rb
+++ b/app/controllers/ci/application_controller.rb
@@ -1,7 +1,5 @@
module Ci
class ApplicationController < ::ApplicationController
- before_action :check_enable_flag!
-
def self.railtie_helpers_paths
"app/helpers/ci"
end
@@ -10,21 +8,6 @@ module Ci
private
- def check_enable_flag!
- unless current_application_settings.ci_enabled
- redirect_to(disabled_ci_projects_path)
- return
- end
- end
-
- def authenticate_public_page!
- unless project.public
- authenticate_user!
-
- return access_denied! unless can?(current_user, :read_project, gl_project)
- end
- end
-
def authenticate_token!
unless project.valid_token?(params[:token])
return head(403)
diff --git a/app/controllers/ci/builds_controller.rb b/app/controllers/ci/builds_controller.rb
deleted file mode 100644
index 80ee8666792..00000000000
--- a/app/controllers/ci/builds_controller.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-module Ci
- class BuildsController < Ci::ApplicationController
- before_action :authenticate_user!, except: [:status, :show]
- before_action :authenticate_public_page!, only: :show
- before_action :project
- before_action :authorize_access_project!, except: [:status, :show]
- before_action :authorize_manage_project!, except: [:status, :show, :retry, :cancel]
- before_action :authorize_manage_builds!, only: [:retry, :cancel]
- before_action :build, except: [:show]
- layout 'ci/build'
-
- def show
- if params[:id] =~ /\A\d+\Z/
- @build = build
- else
- # try to find commit by sha
- commit = commit_by_sha
-
- if commit
- # Redirect to commit page
- redirect_to ci_project_ref_commit_path(@project, @build.commit.ref, @build.commit.sha)
- return
- end
- end
-
- raise ActiveRecord::RecordNotFound unless @build
-
- @builds = @project.commits.find_by_sha(@build.sha).builds.order('id DESC')
- @builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20)
- @commit = @build.commit
-
- respond_to do |format|
- format.html
- format.json do
- render json: @build.to_json(methods: :trace_html)
- end
- end
- end
-
- def retry
- if @build.commands.blank?
- return page_404
- end
-
- build = Ci::Build.retry(@build)
-
- if params[:return_to]
- redirect_to URI.parse(params[:return_to]).path
- else
- redirect_to ci_project_build_path(project, build)
- end
- end
-
- def status
- render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
- end
-
- def cancel
- @build.cancel
-
- redirect_to ci_project_build_path(@project, @build)
- end
-
- protected
-
- def project
- @project = Ci::Project.find(params[:project_id])
- end
-
- def build
- @build ||= project.builds.unscoped.find_by(id: params[:id])
- end
-
- def commit_by_sha
- @project.commits.find_by(sha: params[:id])
- end
- end
-end
diff --git a/app/controllers/ci/commits_controller.rb b/app/controllers/ci/commits_controller.rb
deleted file mode 100644
index 7a0a500fbe6..00000000000
--- a/app/controllers/ci/commits_controller.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-module Ci
- class CommitsController < Ci::ApplicationController
- before_action :authenticate_user!, except: [:status, :show]
- before_action :authenticate_public_page!, only: :show
- before_action :project
- before_action :authorize_access_project!, except: [:status, :show, :cancel]
- before_action :authorize_manage_builds!, only: [:cancel]
- before_action :commit, only: :show
- layout 'ci/commit'
-
- def show
- @builds = @commit.builds
- end
-
- def status
- commit = Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id])
- render json: commit.to_json(only: [:id, :sha], methods: [:status, :coverage])
- rescue ActiveRecord::RecordNotFound
- render json: { status: "not_found" }
- end
-
- def cancel
- commit.builds.running_or_pending.each(&:cancel)
-
- redirect_to ci_project_ref_commits_path(project, commit.ref, commit.sha)
- end
-
- private
-
- def project
- @project ||= Ci::Project.find(params[:project_id])
- end
-
- def commit
- @commit ||= Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id])
- end
- end
-end
diff --git a/app/controllers/ci/events_controller.rb b/app/controllers/ci/events_controller.rb
deleted file mode 100644
index 89b784a1e89..00000000000
--- a/app/controllers/ci/events_controller.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module Ci
- class EventsController < Ci::ApplicationController
- EVENTS_PER_PAGE = 50
-
- before_action :authenticate_user!
- before_action :project
- before_action :authorize_manage_project!
-
- layout 'ci/project'
-
- def index
- @events = project.events.order("created_at DESC").page(params[:page]).per(EVENTS_PER_PAGE)
- end
-
- private
-
- def project
- @project ||= Ci::Project.find(params[:project_id])
- end
- end
-end
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index a81e4e319ff..a4f6aff49b4 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -15,10 +15,10 @@ module Ci
@builds = @config_processor.builds
@status = true
end
- rescue Ci::GitlabCiYamlProcessor::ValidationError => e
+ rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
@error = e.message
@status = false
- rescue Exception => e
+ rescue
@error = "Undefined error"
@status = false
end
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
index 6111753a7fb..8406399fb60 100644
--- a/app/controllers/ci/projects_controller.rb
+++ b/app/controllers/ci/projects_controller.rb
@@ -1,48 +1,15 @@
module Ci
class ProjectsController < Ci::ApplicationController
- before_action :authenticate_user!, except: [:build, :badge, :show]
- before_action :authenticate_public_page!, only: :show
- before_action :project, only: [:build, :show, :badge, :edit, :update, :destroy, :toggle_shared_runners, :dumped_yaml]
- before_action :authorize_access_project!, except: [:build, :badge, :show, :new, :disabled]
- before_action :authorize_manage_project!, only: [:edit, :update, :destroy, :toggle_shared_runners, :dumped_yaml]
- before_action :authenticate_token!, only: [:build]
+ before_action :project, except: [:index]
+ before_action :authenticate_user!, except: [:index, :build, :badge]
+ before_action :authorize_access_project!, except: [:index, :badge]
+ before_action :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml]
before_action :no_cache, only: [:badge]
- skip_before_action :check_enable_flag!, only: [:disabled]
- protect_from_forgery except: :build
-
- layout 'ci/project', except: [:index, :disabled]
-
- def disabled
- end
+ protect_from_forgery
def show
- @ref = params[:ref]
-
- @commits = @project.commits.reverse_order
- @commits = @commits.where(ref: @ref) if @ref
- @commits = @commits.page(params[:page]).per(20)
- end
-
- def edit
- end
-
- def update
- if project.update_attributes(project_params)
- Ci::EventService.new.change_project_settings(current_user, project)
-
- redirect_to :back, notice: 'Project was successfully updated.'
- else
- render action: "edit"
- end
- end
-
- def destroy
- project.gl_project.gitlab_ci_service.update_attributes(active: false)
- project.destroy
-
- Ci::EventService.new.remove_project(current_user, project)
-
- redirect_to ci_projects_url
+ # Temporary compatibility with CI badges pointing to CI project page
+ redirect_to namespace_project_path(project.gl_project.namespace, project.gl_project)
end
# Project status badge
@@ -55,11 +22,8 @@ module Ci
def toggle_shared_runners
project.toggle!(:shared_runners_enabled)
- redirect_to :back
- end
- def dumped_yaml
- send_data @project.generated_yaml_config, filename: '.gitlab-ci.yml'
+ redirect_to namespace_project_runners_path(project.gl_project.namespace, project.gl_project)
end
protected
@@ -73,12 +37,5 @@ module Ci
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
end
-
- def project_params
- params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build,
- :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients,
- :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token,
- { variables_attributes: [:id, :key, :value, :_destroy] })
- end
end
end
diff --git a/app/controllers/ci/runner_projects_controller.rb b/app/controllers/ci/runner_projects_controller.rb
index a8bdd5bb362..9d555313369 100644
--- a/app/controllers/ci/runner_projects_controller.rb
+++ b/app/controllers/ci/runner_projects_controller.rb
@@ -4,17 +4,17 @@ module Ci
before_action :project
before_action :authorize_manage_project!
- layout 'ci/project'
-
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
return head(403) unless current_user.ci_authorized_runners.include?(@runner)
+ path = runners_path(@project.gl_project)
+
if @runner.assign_to(project, current_user)
- redirect_to ci_project_runners_path(project)
+ redirect_to path
else
- redirect_to ci_project_runners_path(project), alert: 'Failed adding runner to project'
+ redirect_to path, alert: 'Failed adding runner to project'
end
end
@@ -22,7 +22,7 @@ module Ci
runner_project = project.runner_projects.find(params[:id])
runner_project.destroy
- redirect_to ci_project_runners_path(project)
+ redirect_to runners_path(@project.gl_project)
end
private
diff --git a/app/controllers/ci/runners_controller.rb b/app/controllers/ci/runners_controller.rb
deleted file mode 100644
index a672370302b..00000000000
--- a/app/controllers/ci/runners_controller.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-module Ci
- class RunnersController < Ci::ApplicationController
- before_action :authenticate_user!
- before_action :project
- before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
- before_action :authorize_access_project!
- before_action :authorize_manage_project!
-
- layout 'ci/project'
-
- def index
- @runners = @project.runners.order('id DESC')
- @specific_runners =
- Ci::Runner.specific.includes(:runner_projects).
- where(Ci::RunnerProject.table_name => { project_id: current_user.authorized_projects } ).
- where.not(id: @runners).order("#{Ci::Runner.table_name}.id DESC").page(params[:page]).per(20)
- @shared_runners = Ci::Runner.shared.active
- @shared_runners_count = @shared_runners.count(:all)
- end
-
- def edit
- end
-
- def update
- if @runner.update_attributes(runner_params)
- redirect_to edit_ci_project_runner_path(@project, @runner), notice: 'Runner was successfully updated.'
- else
- redirect_to edit_ci_project_runner_path(@project, @runner), alert: 'Runner was not updated.'
- end
- end
-
- def destroy
- if @runner.only_for?(@project)
- @runner.destroy
- end
-
- redirect_to ci_project_runners_path(@project)
- end
-
- def resume
- if @runner.update_attributes(active: true)
- redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.'
- else
- redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.'
- end
- end
-
- def pause
- if @runner.update_attributes(active: false)
- redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.'
- else
- redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.'
- end
- end
-
- def show
- end
-
- protected
-
- def project
- @project = Ci::Project.find(params[:project_id])
- end
-
- def set_runner
- @runner ||= @project.runners.find(params[:id])
- end
-
- def runner_params
- params.require(:runner).permit(:description, :tag_list, :contacted_at, :active)
- end
- end
-end
diff --git a/app/controllers/ci/services_controller.rb b/app/controllers/ci/services_controller.rb
deleted file mode 100644
index 52c96a34ce8..00000000000
--- a/app/controllers/ci/services_controller.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-module Ci
- class ServicesController < Ci::ApplicationController
- before_action :authenticate_user!
- before_action :project
- before_action :authorize_access_project!
- before_action :authorize_manage_project!
- before_action :service, only: [:edit, :update, :test]
-
- respond_to :html
-
- layout 'ci/project'
-
- def index
- @project.build_missing_services
- @services = @project.services.reload
- end
-
- def edit
- end
-
- def update
- if @service.update_attributes(service_params)
- redirect_to edit_ci_project_service_path(@project, @service.to_param)
- else
- render 'edit'
- end
- end
-
- def test
- last_build = @project.builds.last
-
- if @service.execute(last_build)
- message = { notice: 'We successfully tested the service' }
- else
- message = { alert: 'We tried to test the service but error occurred' }
- end
-
- redirect_to :back, message
- end
-
- private
-
- def project
- @project = Ci::Project.find(params[:project_id])
- end
-
- def service
- @service ||= @project.services.find { |service| service.to_param == params[:id] }
- end
-
- def service_params
- params.require(:service).permit(
- :type, :active, :webhook, :notify_only_broken_builds,
- :email_recipients, :email_only_broken_builds, :email_add_pusher,
- :hipchat_token, :hipchat_room, :hipchat_server
- )
- end
- end
-end
diff --git a/app/controllers/ci/triggers_controller.rb b/app/controllers/ci/triggers_controller.rb
deleted file mode 100644
index a39cc5d3a56..00000000000
--- a/app/controllers/ci/triggers_controller.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-module Ci
- class TriggersController < Ci::ApplicationController
- before_action :authenticate_user!
- before_action :project
- before_action :authorize_access_project!
- before_action :authorize_manage_project!
-
- layout 'ci/project'
-
- def index
- @triggers = @project.triggers
- @trigger = Ci::Trigger.new
- end
-
- def create
- @trigger = @project.triggers.new
- @trigger.save
-
- if @trigger.valid?
- redirect_to ci_project_triggers_path(@project)
- else
- @triggers = @project.triggers.select(&:persisted?)
- render :index
- end
- end
-
- def destroy
- trigger.destroy
-
- redirect_to ci_project_triggers_path(@project)
- end
-
- private
-
- def trigger
- @trigger ||= @project.triggers.find(params[:id])
- end
-
- def project
- @project = Ci::Project.find(params[:project_id])
- end
- end
-end
diff --git a/app/controllers/ci/variables_controller.rb b/app/controllers/ci/variables_controller.rb
deleted file mode 100644
index 9c6c775fde8..00000000000
--- a/app/controllers/ci/variables_controller.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module Ci
- class VariablesController < Ci::ApplicationController
- before_action :authenticate_user!
- before_action :project
- before_action :authorize_access_project!
- before_action :authorize_manage_project!
-
- layout 'ci/project'
-
- def show
- end
-
- def update
- if project.update_attributes(project_params)
- Ci::EventService.new.change_project_settings(current_user, project)
-
- redirect_to ci_project_variables_path(project), notice: 'Variables were successfully updated.'
- else
- render action: 'show'
- end
- end
-
- private
-
- def project
- @project ||= Ci::Project.find(params[:project_id])
- end
-
- def project_params
- params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] })
- end
- end
-end
diff --git a/app/controllers/ci/web_hooks_controller.rb b/app/controllers/ci/web_hooks_controller.rb
deleted file mode 100644
index 24074a6d9ac..00000000000
--- a/app/controllers/ci/web_hooks_controller.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-module Ci
- class WebHooksController < Ci::ApplicationController
- before_action :authenticate_user!
- before_action :project
- before_action :authorize_access_project!
- before_action :authorize_manage_project!
-
- layout 'ci/project'
-
- def index
- @web_hooks = @project.web_hooks
- @web_hook = Ci::WebHook.new
- end
-
- def create
- @web_hook = @project.web_hooks.new(web_hook_params)
- @web_hook.save
-
- if @web_hook.valid?
- redirect_to ci_project_web_hooks_path(@project)
- else
- @web_hooks = @project.web_hooks.select(&:persisted?)
- render :index
- end
- end
-
- def test
- Ci::TestHookService.new.execute(hook, current_user)
-
- redirect_to :back
- end
-
- def destroy
- hook.destroy
-
- redirect_to ci_project_web_hooks_path(@project)
- end
-
- private
-
- def hook
- @web_hook ||= @project.web_hooks.find(params[:id])
- end
-
- def project
- @project = Ci::Project.find(params[:project_id])
- end
-
- def web_hook_params
- params.require(:web_hook).permit(:url)
- end
- end
-end
diff --git a/app/controllers/concerns/creates_merge_request_for_commit.rb b/app/controllers/concerns/creates_merge_request_for_commit.rb
new file mode 100644
index 00000000000..c7527822158
--- /dev/null
+++ b/app/controllers/concerns/creates_merge_request_for_commit.rb
@@ -0,0 +1,28 @@
+module CreatesMergeRequestForCommit
+ extend ActiveSupport::Concern
+
+ def new_merge_request_path
+ if @project.forked?
+ target_project = @project.forked_from_project || @project
+ target_branch = target_project.repository.root_ref
+ else
+ target_project = @project
+ target_branch = @ref
+ end
+
+ new_namespace_project_merge_request_path(
+ @project.namespace,
+ @project,
+ merge_request: {
+ source_project_id: @project.id,
+ target_project_id: target_project.id,
+ source_branch: @new_branch,
+ target_branch: target_branch
+ }
+ )
+ end
+
+ def create_merge_request?
+ params[:create_merge_request] && @new_branch != @ref
+ end
+end
diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb
new file mode 100644
index 00000000000..3e4c0e63601
--- /dev/null
+++ b/app/controllers/concerns/global_milestones.rb
@@ -0,0 +1,21 @@
+module GlobalMilestones
+ extend ActiveSupport::Concern
+
+ def milestones
+ epoch = DateTime.parse('1970-01-01')
+ @milestones = MilestonesFinder.new.execute(@projects, params)
+ @milestones = GlobalMilestone.build_collection(@milestones)
+ @milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
+ @milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE)
+ end
+
+ def milestone
+ milestones = Milestone.of_projects(@projects).where(title: params[:title])
+
+ if milestones.present?
+ @milestone = GlobalMilestone.new(params[:title], milestones)
+ else
+ render_404
+ end
+ end
+end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
new file mode 100644
index 00000000000..effd4721949
--- /dev/null
+++ b/app/controllers/concerns/issues_action.rb
@@ -0,0 +1,14 @@
+module IssuesAction
+ extend ActiveSupport::Concern
+
+ def issues
+ @issues = get_issues_collection
+ @issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE)
+ @issues = @issues.preload(:author, :project)
+
+ respond_to do |format|
+ format.html
+ format.atom { render layout: false }
+ end
+ end
+end
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
new file mode 100644
index 00000000000..f7a25111db9
--- /dev/null
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -0,0 +1,9 @@
+module MergeRequestsAction
+ extend ActiveSupport::Concern
+
+ def merge_requests
+ @merge_requests = get_merge_requests_collection
+ @merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE)
+ @merge_requests = @merge_requests.preload(:author, :target_project)
+ end
+end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 53896d4f2c7..2bdce0f8a00 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -1,34 +1,19 @@
class Dashboard::MilestonesController < Dashboard::ApplicationController
- before_action :load_projects
+ include GlobalMilestones
+
+ before_action :projects
+ before_action :milestones, only: [:index]
+ before_action :milestone, only: [:show]
def index
- project_milestones = case params[:state]
- when 'all'; state
- when 'closed'; state('closed')
- else state('active')
- end
- @dashboard_milestones = Milestones::GroupService.new(project_milestones).execute
- @dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(PER_PAGE)
end
def show
- project_milestones = Milestone.where(project_id: @projects).order("due_date ASC")
- @dashboard_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
end
private
- def load_projects
- @projects = current_user.authorized_projects.sorted_by_activity.non_archived
- end
-
- def title
- params[:title]
- end
-
- def state(state = nil)
- conditions = { project_id: @projects }
- conditions.reverse_merge!(state: state) if state
- Milestone.where(conditions).order("title ASC")
+ def projects
+ @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
end
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 467d0f81aca..58e9049f158 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -20,6 +20,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = current_user.starred_projects
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
+ @last_push = current_user.recent_push
@groups = []
respond_to do |format|
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 4ebb3d7276e..087da935087 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -1,25 +1,12 @@
class DashboardController < Dashboard::ApplicationController
+ include IssuesAction
+ include MergeRequestsAction
+
before_action :event_filter, only: :activity
+ before_action :projects, only: [:issues, :merge_requests]
respond_to :html
- def merge_requests
- @merge_requests = get_merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
- @merge_requests = @merge_requests.preload(:author, :target_project)
- end
-
- def issues
- @issues = get_issues_collection
- @issues = @issues.page(params[:page]).per(PER_PAGE)
- @issues = @issues.preload(:author, :project)
-
- respond_to do |format|
- format.html
- format.atom { render layout: false }
- end
- end
-
def activity
@last_push = current_user.recent_push
@@ -47,4 +34,8 @@ class DashboardController < Dashboard::ApplicationController
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
+
+ def projects
+ @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
+ end
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 6878d4bc07e..be801858eaf 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -1,8 +1,13 @@
class Groups::ApplicationController < ApplicationController
layout 'group'
+ before_action :group
private
-
+
+ def group
+ @group ||= Group.find_by(path: params[:group_id])
+ end
+
def authorize_read_group!
unless @group and can?(current_user, :read_group, @group)
if current_user.nil?
@@ -12,13 +17,13 @@ class Groups::ApplicationController < ApplicationController
end
end
end
-
+
def authorize_admin_group!
unless can?(current_user, :admin_group, group)
return render_404
end
end
-
+
def authorize_admin_group_member!
unless can?(current_user, :admin_group_member, group)
return render_403
diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb
index 6aa64222f77..76c87366baa 100644
--- a/app/controllers/groups/avatars_controller.rb
+++ b/app/controllers/groups/avatars_controller.rb
@@ -1,8 +1,6 @@
-class Groups::AvatarsController < ApplicationController
+class Groups::AvatarsController < Groups::ApplicationController
def destroy
- @group = Group.find_by(path: params[:group_id])
@group.remove_avatar!
-
@group.save
redirect_to edit_group_path(@group)
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 91518c44a98..0e902c4bb43 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,11 +1,9 @@
class Groups::GroupMembersController < Groups::ApplicationController
skip_before_action :authenticate_user!, only: [:index]
- before_action :group
# Authorize
before_action :authorize_read_group!
- before_action :authorize_admin_group!, except: [:index, :leave]
- before_action :authorize_admin_group_member!, only: [:create, :resend_invite]
+ before_action :authorize_admin_group_member!, except: [:index, :leave]
def index
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@@ -18,7 +16,8 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
@members = @members.order('access_level DESC').page(params[:page]).per(50)
- @group_member = GroupMember.new
+
+ @group_member = @group.group_members.new
end
def create
@@ -28,24 +27,23 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def update
- @member = @group.group_members.find(params[:id])
+ @group_member = @group.group_members.find(params[:id])
- return render_403 unless can?(current_user, :update_group_member, @member)
+ return render_403 unless can?(current_user, :update_group_member, @group_member)
- @member.update_attributes(member_params)
+ @group_member.update_attributes(member_params)
end
def destroy
@group_member = @group.group_members.find(params[:id])
- if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner.
- @group_member.destroy
- respond_to do |format|
- format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
- format.js { render nothing: true }
- end
- else
- return render_403
+ return render_403 unless can?(current_user, :destroy_group_member, @group_member)
+
+ @group_member.destroy
+
+ respond_to do |format|
+ format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
+ format.js { render nothing: true }
end
end
@@ -64,10 +62,11 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def leave
- @group_member = @group.group_members.where(user_id: current_user.id).first
+ @group_member = @group.group_members.find_by(user_id: current_user)
if can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy
+
redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
else
if @group.last_owner?(current_user)
@@ -80,10 +79,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
protected
- def group
- @group ||= Group.find_by(path: params[:group_id])
- end
-
def member_params
params.require(:group_member).permit(:access_level, :user_id)
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 669f7f3126d..0c2a350bc39 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -1,54 +1,55 @@
class Groups::MilestonesController < Groups::ApplicationController
- before_action :authorize_group_milestone!, only: :update
+ include GlobalMilestones
+
+ before_action :projects
+ before_action :milestones, only: [:index]
+ before_action :milestone, only: [:show, :update]
+ before_action :authorize_group_milestone!, only: [:create, :update]
def index
- project_milestones = case params[:state]
- when 'all'; state
- when 'closed'; state('closed')
- else state('active')
- end
- @group_milestones = Milestones::GroupService.new(project_milestones).execute
- @group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(PER_PAGE)
end
- def show
- project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC")
- @group_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
+ def new
+ @milestone = Milestone.new
end
- def update
- project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC")
- @group_milestones = Milestones::GroupService.new(project_milestones).milestone(title)
+ def create
+ project_ids = params[:milestone][:project_ids]
+ title = milestone_params[:title]
- @group_milestones.milestones.each do |milestone|
- Milestones::UpdateService.new(milestone.project, current_user, params[:milestone]).execute(milestone)
+ @group.projects.where(id: project_ids).each do |project|
+ Milestones::CreateService.new(project, current_user, milestone_params).execute
end
- respond_to do |format|
- format.js
- format.html do
- redirect_to group_milestones_path(group)
- end
+ redirect_to milestone_path(title)
+ end
+
+ def show
+ end
+
+ def update
+ @milestone.milestones.each do |milestone|
+ Milestones::UpdateService.new(milestone.project, current_user, milestone_params).execute(milestone)
end
+
+ redirect_back_or_default(default: milestone_path(@milestone.title))
end
private
- def group
- @group ||= Group.find_by(path: params[:group_id])
+ def authorize_group_milestone!
+ return render_404 unless can?(current_user, :admin_milestones, group)
end
- def title
- params[:title]
+ def milestone_params
+ params.require(:milestone).permit(:title, :description, :due_date, :state_event)
end
- def state(state = nil)
- conditions = { project_id: group.projects }
- conditions.reverse_merge!(state: state) if state
- Milestone.where(conditions).order("title ASC")
+ def milestone_path(title)
+ group_milestone_path(@group, title.to_slug.to_s, title: title)
end
- def authorize_group_milestone!
- return render_404 unless can?(current_user, :admin_group, group)
+ def projects
+ @projects ||= @group.projects
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 524218290c6..fb26a4e6fc3 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -1,15 +1,18 @@
class GroupsController < Groups::ApplicationController
+ include IssuesAction
+ include MergeRequestsAction
+
skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests]
respond_to :html
before_action :group, except: [:new, :create]
# Authorize
- before_action :authorize_read_group!, except: [:show, :new, :create]
+ before_action :authorize_read_group!, except: [:show, :new, :create, :autocomplete]
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
before_action :authorize_create_group!, only: [:new, :create]
# Load group projects
- before_action :load_projects, except: [:new, :create, :projects, :edit, :update]
+ before_action :load_projects, except: [:new, :create, :projects, :edit, :update, :autocomplete]
before_action :event_filter, only: :show
layout :determine_layout
@@ -53,23 +56,6 @@ class GroupsController < Groups::ApplicationController
end
end
- def merge_requests
- @merge_requests = get_merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
- @merge_requests = @merge_requests.preload(:author, :target_project)
- end
-
- def issues
- @issues = get_issues_collection
- @issues = @issues.page(params[:page]).per(PER_PAGE)
- @issues = @issues.preload(:author, :project)
-
- respond_to do |format|
- format.html
- format.atom { render layout: false }
- end
- end
-
def edit
end
@@ -88,7 +74,7 @@ class GroupsController < Groups::ApplicationController
def destroy
DestroyGroupService.new(@group, current_user).execute
- redirect_to root_path, alert: "Group '#{@group.name} was deleted."
+ redirect_to root_path, alert: "Group '#{@group.name}' was successfully deleted."
end
protected
@@ -133,7 +119,7 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar)
+ params.require(:group).permit(:name, :description, :path, :avatar, :public)
end
def load_events
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index f84f85a7df8..25e58724860 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -62,7 +62,7 @@ class Import::BitbucketController < Import::BaseController
end
def verify_bitbucket_import_enabled
- not_found! unless bitbucket_import_enabled?
+ render_404 unless bitbucket_import_enabled?
end
def bitbucket_auth
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 849646cd665..18300390851 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -99,6 +99,6 @@ class Import::FogbugzController < Import::BaseController
end
def verify_fogbugz_import_enabled
- not_found! unless fogbugz_import_enabled?
+ render_404 unless fogbugz_import_enabled?
end
end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index f21fbd9ecca..67bf4190e7e 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -11,10 +11,6 @@ class Import::GithubController < Import::BaseController
def status
@repos = client.repos
- client.orgs.each do |org|
- @repos += client.org_repos(org.login)
- end
-
@already_added_projects = current_user.created_projects.where(import_type: "github")
already_added_projects_names = @already_added_projects.pluck(:import_source)
@@ -47,7 +43,7 @@ class Import::GithubController < Import::BaseController
end
def verify_github_import_enabled
- not_found! unless github_import_enabled?
+ render_404 unless github_import_enabled?
end
def github_auth
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index 27af19f5f61..23a396e8084 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -44,7 +44,7 @@ class Import::GitlabController < Import::BaseController
end
def verify_gitlab_import_enabled
- not_found! unless gitlab_import_enabled?
+ render_404 unless gitlab_import_enabled?
end
def gitlab_auth
diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb
index f24cdb3709a..eecbe380c9e 100644
--- a/app/controllers/import/gitorious_controller.rb
+++ b/app/controllers/import/gitorious_controller.rb
@@ -42,7 +42,7 @@ class Import::GitoriousController < Import::BaseController
end
def verify_gitorious_import_enabled
- not_found! unless gitorious_import_enabled?
+ render_404 unless gitorious_import_enabled?
end
end
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
index 82fadeb7e83..e0de31f2251 100644
--- a/app/controllers/import/google_code_controller.rb
+++ b/app/controllers/import/google_code_controller.rb
@@ -10,18 +10,18 @@ class Import::GoogleCodeController < Import::BaseController
dump_file = params[:dump_file]
unless dump_file.respond_to?(:read)
- return redirect_to :back, alert: "You need to upload a Google Takeout archive."
+ return redirect_back_or_default(options: { alert: "You need to upload a Google Takeout archive." })
end
begin
dump = JSON.parse(dump_file.read)
rescue
- return redirect_to :back, alert: "The uploaded file is not a valid Google Takeout archive."
+ return redirect_back_or_default(options: { alert: "The uploaded file is not a valid Google Takeout archive." })
end
client = Gitlab::GoogleCodeImport::Client.new(dump)
unless client.valid?
- return redirect_to :back, alert: "The uploaded file is not a valid Google Takeout archive."
+ return redirect_back_or_default(options: { alert: "The uploaded file is not a valid Google Takeout archive." })
end
session[:google_code_dump] = dump
@@ -106,7 +106,7 @@ class Import::GoogleCodeController < Import::BaseController
end
def verify_google_code_import_enabled
- not_found! unless google_code_import_enabled?
+ render_404 unless google_code_import_enabled?
end
def user_map
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 8ef10a17f55..94bb108c5f5 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -14,7 +14,7 @@ class InvitesController < ApplicationController
redirect_to path, notice: "You have been granted #{member.human_access} access to #{label}."
else
- redirect_to :back, alert: "The invitation could not be accepted."
+ redirect_back_or_default(options: { alert: "The invitation could not be accepted." })
end
end
@@ -31,7 +31,7 @@ class InvitesController < ApplicationController
redirect_to path, notice: "You have declined the invitation to join #{label}."
else
- redirect_to :back, alert: "The invitation could not be declined."
+ redirect_back_or_default(options: { alert: "The invitation could not be declined." })
end
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 523264b8ea9..f809fa7500a 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -71,7 +71,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return
end
end
- rescue Gitlab::OAuth::SignupDisabledError => e
+ rescue Gitlab::OAuth::SignupDisabledError
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
@@ -80,7 +80,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
flash[:notice] = message
-
+
redirect_to new_user_session_path
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 8450ba31021..2025158d065 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -1,41 +1,7 @@
class PasswordsController < Devise::PasswordsController
-
- def create
- email = resource_params[:email]
- resource_found = resource_class.find_by_email(email)
- if resource_found && resource_found.ldap_user?
- flash[:alert] = "Cannot reset password for LDAP user."
- respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) and return
- end
-
- self.resource = resource_class.send_reset_password_instructions(resource_params)
- if successfully_sent?(resource)
- respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name))
- else
- respond_with(resource)
- end
- end
-
- # After a user resets their password, prompt for 2FA code if enabled instead
- # of signing in automatically
- #
- # See http://git.io/vURrI
- def update
- super do |resource|
- # TODO (rspeicher): In Devise master (> 3.4.1), we can set
- # `Devise.sign_in_after_reset_password = false` and avoid this mess.
- if resource.errors.empty? && resource.try(:two_factor_enabled?)
- resource.unlock_access! if unlockable?(resource)
-
- # Since we are not signing this user in, we use the :updated_not_active
- # message which only contains "Your password was changed successfully."
- set_flash_message(:notice, :updated_not_active) if is_flashing_format?
-
- # Redirect to sign in so they can enter 2FA code
- respond_with(resource, location: new_session_path(resource)) and return
- end
- end
- end
+ before_action :resource_from_email, only: [:create]
+ before_action :prevent_ldap_reset, only: [:create]
+ before_action :throttle_reset, only: [:create]
def edit
super
@@ -56,4 +22,25 @@ class PasswordsController < Devise::PasswordsController
end
end
end
+
+ protected
+
+ def resource_from_email
+ email = resource_params[:email]
+ self.resource = resource_class.find_by_email(email)
+ end
+
+ def prevent_ldap_reset
+ return unless resource && resource.ldap_user?
+
+ redirect_to after_sending_reset_password_instructions_path_for(resource_name),
+ alert: "Cannot reset password for LDAP user."
+ end
+
+ def throttle_reset
+ return unless resource && resource.recently_sent_password_reset?
+
+ redirect_to new_password_path(resource_name),
+ alert: I18n.t('devise.passwords.recently_reset')
+ end
end
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 22423651c17..1fd1d6882df 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -29,7 +29,7 @@ class Profiles::NotificationsController < Profiles::ApplicationController
flash[:alert] = "Failed to save new settings"
end
- redirect_to :back
+ redirect_back_or_default(default: profile_notifications_path)
end
format.js
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index f83b4abd1e2..a9a06ecc808 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -31,6 +31,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
def preferences_params
params.require(:user).permit(
:color_scheme_id,
+ :layout,
:dashboard,
:project_view,
:theme_id
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 26a4de15462..28803164fcf 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -26,7 +26,7 @@ class ProfilesController < Profiles::ApplicationController
end
respond_to do |format|
- format.html { redirect_to :back }
+ format.html { redirect_back_or_default(default: { action: 'show' }) }
end
end
@@ -70,6 +70,7 @@ class ProfilesController < Profiles::ApplicationController
:email,
:hide_no_password,
:hide_no_ssh_key,
+ :hide_project_limit,
:linkedin,
:location,
:name,
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 48c922f450c..d3f926b62bc 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -29,6 +29,10 @@ class Projects::ApplicationController < ApplicationController
private
def ci_enabled
- return render_404 unless @project.gitlab_ci?
+ return render_404 unless @project.builds_enabled?
+ end
+
+ def ci_project
+ @ci_project ||= @project.ensure_gitlab_ci_project
end
end
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 9c3763d5934..548f1b9ebfe 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -12,7 +12,7 @@ class Projects::AvatarsController < Projects::ApplicationController
filename: @blob.name
)
else
- not_found!
+ render_404
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 8776721d243..31a33bfd237 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -1,6 +1,7 @@
# Controller for viewing a file's blame
class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
+ include CreatesMergeRequestForCommit
include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path
@@ -8,7 +9,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code!
- before_action :authorize_push_code!, only: [:destroy]
+ before_action :authorize_push_code!, only: [:destroy, :create]
before_action :assign_blob_vars
before_action :commit, except: [:new, :create]
before_action :blob, except: [:new, :create]
@@ -22,21 +23,9 @@ class Projects::BlobController < Projects::ApplicationController
end
def create
- result = Files::CreateService.new(@project, current_user, @commit_params).execute
-
- if result[:status] == :success
- flash[:notice] = "Your changes have been successfully committed"
- respond_to do |format|
- format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) }
- format.json { render json: { message: "success", filePath: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } }
- end
- else
- flash[:alert] = result[:message]
- respond_to do |format|
- format.html { render :new }
- format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } }
- end
- end
+ create_commit(Files::CreateService, success_path: after_create_path,
+ failure_view: :new,
+ failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
end
def show
@@ -47,21 +36,9 @@ class Projects::BlobController < Projects::ApplicationController
end
def update
- result = Files::UpdateService.new(@project, current_user, @commit_params).execute
-
- if result[:status] == :success
- flash[:notice] = "Your changes have been successfully committed"
- respond_to do |format|
- format.html { redirect_to after_edit_path }
- format.json { render json: { message: "success", filePath: after_edit_path } }
- end
- else
- flash[:alert] = result[:message]
- respond_to do |format|
- format.html { render :edit }
- format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } }
- end
- end
+ create_commit(Files::UpdateService, success_path: after_edit_path,
+ failure_view: :edit,
+ failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
end
def preview
@@ -77,7 +54,7 @@ class Projects::BlobController < Projects::ApplicationController
if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed"
- redirect_to namespace_project_tree_path(@project.namespace, @project, @target_branch)
+ redirect_to after_destroy_path
else
flash[:alert] = result[:message]
render :show
@@ -113,14 +90,14 @@ class Projects::BlobController < Projects::ApplicationController
end
end
- return not_found!
+ return render_404
end
end
def commit
@commit = @repository.commit(@ref)
- return not_found! unless @commit
+ return render_404 unless @commit
end
def assign_blob_vars
@@ -128,18 +105,54 @@ class Projects::BlobController < Projects::ApplicationController
@ref, @path = extract_ref(@id)
rescue InvalidPathError
- not_found!
+ render_404
+ end
+
+ def create_commit(service, success_path:, failure_view:, failure_path:)
+ result = service.new(@project, current_user, @commit_params).execute
+
+ if result[:status] == :success
+ flash[:notice] = "Your changes have been successfully committed"
+ respond_to do |format|
+ format.html { redirect_to success_path }
+ format.json { render json: { message: "success", filePath: success_path } }
+ end
+ else
+ flash[:alert] = result[:message]
+ respond_to do |format|
+ format.html { render failure_view }
+ format.json { render json: { message: "failed", filePath: failure_path } }
+ end
+ end
+ end
+
+ def after_create_path
+ @after_create_path ||=
+ if create_merge_request?
+ new_merge_request_path
+ else
+ namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @file_path))
+ end
end
def after_edit_path
@after_edit_path ||=
- if from_merge_request
+ if create_merge_request?
+ new_merge_request_path
+ elsif from_merge_request && @new_branch == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"#file-path-#{hexdigest(@path)}"
- elsif @target_branch.present?
- namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
else
- namespace_project_blob_path(@project.namespace, @project, @id)
+ namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @path))
+ end
+ end
+
+ def after_destroy_path
+ @after_destroy_path ||=
+ if create_merge_request?
+ new_merge_request_path
+ else
+ namespace_project_tree_path(@project.namespace, @project, @new_branch)
end
end
@@ -154,14 +167,14 @@ class Projects::BlobController < Projects::ApplicationController
def editor_variables
@current_branch = @ref
- @target_branch = (sanitized_new_branch_name || @ref)
+ @new_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref
@file_path =
if action_name.to_s == 'create'
if params[:file].present?
params[:file_name] = params[:file].original_filename
end
- File.join(@path, File.basename(params[:file_name]))
+ File.join(@path, params[:file_name])
else
@path
end
@@ -174,7 +187,7 @@ class Projects::BlobController < Projects::ApplicationController
@commit_params = {
file_path: @file_path,
current_branch: @current_branch,
- target_branch: @target_branch,
+ target_branch: @new_branch,
commit_message: params[:commit_message],
file_content: params[:content],
file_content_encoding: params[:encoding]
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 3ac0a75fa70..3c2849a7601 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -3,7 +3,7 @@ class Projects::BranchesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :authorize_push_code!, only: [:create, :destroy]
+ before_action :authorize_push_code!, only: [:new, :create, :destroy]
def index
@sort = params[:sort] || 'name'
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
new file mode 100644
index 00000000000..4638f77b887
--- /dev/null
+++ b/app/controllers/projects/builds_controller.rb
@@ -0,0 +1,106 @@
+class Projects::BuildsController < Projects::ApplicationController
+ before_action :ci_project
+ 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"
+
+ def index
+ @scope = params[:scope]
+ @all_builds = project.ci_builds
+ @builds = @all_builds.order('created_at DESC')
+ @builds =
+ case @scope
+ when 'all'
+ @builds
+ when 'finished'
+ @builds.finished
+ else
+ @builds.running_or_pending.reverse_order
+ end
+ @builds = @builds.page(params[:page]).per(30)
+ end
+
+ def cancel_all
+ @project.ci_builds.running_or_pending.each(&:cancel)
+
+ redirect_to namespace_project_builds_path(project.namespace, project)
+ end
+
+ def show
+ @builds = @ci_project.commits.find_by_sha(@build.sha).builds.order('id DESC')
+ @builds = @builds.where("id not in (?)", @build.id)
+ @commit = @build.commit
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @build.to_json(methods: :trace_html)
+ end
+ end
+ end
+
+ def retry
+ unless @build.retryable?
+ return page_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'
+ 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)
+ end
+
+ private
+
+ def build
+ @build ||= ci_project.builds.unscoped.find_by!(id: params[:id])
+ end
+
+ def artifacts_file
+ build.artifacts_file
+ end
+
+ def build_path(build)
+ namespace_project_build_path(build.gl_project.namespace, build.gl_project, build)
+ end
+
+ def authorize_manage_builds!
+ unless can?(current_user, :manage_builds, project)
+ return page_404
+ end
+ end
+
+ def authorize_download_build_artifacts!
+ unless can?(current_user, :download_build_artifacts, @project)
+ if current_user.nil?
+ return authenticate_user!
+ else
+ return render_404
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/ci_services_controller.rb b/app/controllers/projects/ci_services_controller.rb
new file mode 100644
index 00000000000..550a019e8e2
--- /dev/null
+++ b/app/controllers/projects/ci_services_controller.rb
@@ -0,0 +1,49 @@
+class Projects::CiServicesController < Projects::ApplicationController
+ before_action :ci_project
+ before_action :authorize_admin_project!
+
+ layout "project_settings"
+
+ def index
+ @ci_project.build_missing_services
+ @services = @ci_project.services.reload
+ end
+
+ def edit
+ service
+ end
+
+ def update
+ if service.update_attributes(service_params)
+ redirect_to edit_namespace_project_ci_service_path(@project.namespace, @project, service.to_param)
+ else
+ render 'edit'
+ end
+ end
+
+ def test
+ last_build = @project.ci_builds.last
+
+ if service.execute(last_build)
+ message = { notice: 'We successfully tested the service' }
+ else
+ message = { alert: 'We tried to test the service but error occurred' }
+ end
+
+ redirect_back_or_default(options: message)
+ end
+
+ private
+
+ def service
+ @service ||= @ci_project.services.find { |service| service.to_param == params[:id] }
+ end
+
+ def service_params
+ params.require(:service).permit(
+ :type, :active, :webhook, :notify_only_broken_builds,
+ :email_recipients, :email_only_broken_builds, :email_add_pusher,
+ :hipchat_token, :hipchat_room, :hipchat_server
+ )
+ end
+end
diff --git a/app/controllers/projects/ci_settings_controller.rb b/app/controllers/projects/ci_settings_controller.rb
new file mode 100644
index 00000000000..a263242a850
--- /dev/null
+++ b/app/controllers/projects/ci_settings_controller.rb
@@ -0,0 +1,36 @@
+class Projects::CiSettingsController < Projects::ApplicationController
+ before_action :ci_project
+ before_action :authorize_admin_project!
+
+ layout "project_settings"
+
+ def edit
+ end
+
+ def update
+ if ci_project.update_attributes(project_params)
+ Ci::EventService.new.change_project_settings(current_user, ci_project)
+
+ redirect_to edit_namespace_project_ci_settings_path(project.namespace, project), notice: 'Project was successfully updated.'
+ else
+ render action: "edit"
+ end
+ end
+
+ def destroy
+ ci_project.destroy
+ Ci::EventService.new.remove_project(current_user, ci_project)
+ project.gitlab_ci_service.update_attributes(active: false)
+
+ redirect_to project_path(project), notice: "CI was disabled for this project"
+ end
+
+ protected
+
+ def project_params
+ params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build,
+ :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients,
+ :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token,
+ { variables_attributes: [:id, :key, :value, :_destroy] })
+ end
+end
diff --git a/app/controllers/projects/ci_web_hooks_controller.rb b/app/controllers/projects/ci_web_hooks_controller.rb
new file mode 100644
index 00000000000..a2d470d4a69
--- /dev/null
+++ b/app/controllers/projects/ci_web_hooks_controller.rb
@@ -0,0 +1,45 @@
+class Projects::CiWebHooksController < Projects::ApplicationController
+ before_action :ci_project
+ before_action :authorize_admin_project!
+
+ layout "project_settings"
+
+ def index
+ @web_hooks = @ci_project.web_hooks
+ @web_hook = Ci::WebHook.new
+ end
+
+ def create
+ @web_hook = @ci_project.web_hooks.new(web_hook_params)
+ @web_hook.save
+
+ if @web_hook.valid?
+ redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project)
+ else
+ @web_hooks = @ci_project.web_hooks.select(&:persisted?)
+ render :index
+ end
+ end
+
+ def test
+ Ci::TestHookService.new.execute(hook, current_user)
+
+ redirect_back_or_default(default: { action: 'index' })
+ end
+
+ def destroy
+ hook.destroy
+
+ redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project)
+ end
+
+ private
+
+ def hook
+ @web_hook ||= @ci_project.web_hooks.find(params[:id])
+ end
+
+ def web_hook_params
+ params.require(:web_hook).permit(:url)
+ end
+end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 2fae5057138..3f137440e28 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -4,16 +4,17 @@
class Projects::CommitController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!
+ before_action :authorize_download_code!, except: [:cancel_builds]
+ before_action :authorize_manage_builds!, only: [:cancel_builds]
before_action :commit
+ before_action :authorize_manage_builds!, only: [:cancel_builds, :retry_builds]
+ before_action :define_show_vars, only: [:show, :builds]
def show
return git_not_found! unless @commit
@line_notes = commit.notes.inline
- @diffs = @commit.diffs
@note = @project.build_commit_note(commit)
- @notes_count = commit.notes.count
@notes = commit.notes.not_inline.fresh
@noteable = @commit
@comments_allowed = @reply_allowed = true
@@ -22,8 +23,6 @@ class Projects::CommitController < Projects::ApplicationController
commit_id: @commit.id
}
- @ci_commit = project.ci_commit(commit.sha)
-
respond_to do |format|
format.html
format.diff { render text: @commit.to_diff }
@@ -31,13 +30,57 @@ class Projects::CommitController < Projects::ApplicationController
end
end
+ def builds
+ @ci_project = @project.gitlab_ci_project
+ end
+
+ def cancel_builds
+ ci_commit.builds.running_or_pending.each(&:cancel)
+
+ redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.sha)
+ end
+
+ def retry_builds
+ ci_commit.builds.latest.failed.each do |build|
+ if build.retryable?
+ Ci::Build.retry(build)
+ end
+ end
+
+ redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.sha)
+ end
+
def branches
@branches = @project.repository.branch_names_contains(commit.id)
@tags = @project.repository.tag_names_contains(commit.id)
render layout: false
end
+ private
+
def commit
@commit ||= @project.commit(params[:id])
end
+
+ def ci_commit
+ @ci_commit ||= project.ci_commit(commit.sha)
+ end
+
+ def define_show_vars
+ if params[:w].to_i == 1
+ @diffs = commit.diffs({ ignore_whitespace_change: true })
+ else
+ @diffs = commit.diffs
+ end
+
+ @notes_count = commit.notes.count
+
+ @builds = ci_commit.builds if ci_commit
+ end
+
+ def authorize_manage_builds!
+ unless can?(current_user, :manage_builds, project)
+ return page_404
+ end
+ end
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index d1c15174aea..58fb946dbc2 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -12,7 +12,7 @@ class Projects::CommitsController < Projects::ApplicationController
@limit, @offset = (params[:limit] || 40), (params[:offset] || 0)
@commits = @repo.commits(@ref, @path, @limit, @offset)
- @note_counts = Note.where(commit_id: @commits.map(&:id)).
+ @note_counts = project.notes.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
respond_to do |format|
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index d15004f93a6..5200d609cc9 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -12,14 +12,16 @@ class Projects::CompareController < Projects::ApplicationController
def show
base_ref = Addressable::URI.unescape(params[:from])
@ref = head_ref = Addressable::URI.unescape(params[:to])
+ diff_options = { ignore_whitespace_change: true } if params[:w] == '1'
compare_result = CompareService.new.
- execute(@project, head_ref, @project, base_ref)
+ execute(@project, head_ref, @project, base_ref, diff_options)
if compare_result
- @commits = compare_result.commits
+ @commits = Commit.decorate(compare_result.commits, @project)
@diffs = compare_result.diffs
- @commit = @commits.last
+ @commit = @project.commit(head_ref)
+ @first_commit = @project.commit(base_ref)
@line_notes = []
end
end
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 40e2b37912b..7d09288bc80 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -46,7 +46,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
def disable
@project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy
- redirect_to :back
+ redirect_back_or_default(default: { action: 'index' })
end
protected
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 4e5b4125f5a..6a62880cb71 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -25,19 +25,18 @@ class Projects::HooksController < Projects::ApplicationController
def test
if !@project.empty_repo?
- status = TestHookService.new.execute(hook, current_user)
+ status, message = TestHookService.new.execute(hook, current_user)
if status
flash[:notice] = 'Hook successfully executed.'
else
- flash[:alert] = 'Hook execution failed. '\
- 'Ensure hook URL is correct and service is up.'
+ flash[:alert] = "Hook execution failed: #{message}"
end
else
flash[:alert] = 'Hook execution failed. Ensure the project has commits.'
end
- redirect_to :back
+ redirect_back_or_default(default: { action: 'index' })
end
def destroy
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 066b66014f8..fb8788f0818 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -28,8 +28,8 @@ class Projects::ImportsController < Projects::ApplicationController
if @project.import_finished?
redirect_to(project_path(@project)) and return
else
- redirect_to new_namespace_project_import_path(@project.namespace,
- @project) && return
+ redirect_to(new_namespace_project_import_path(@project.namespace,
+ @project)) and return
end
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 0f89f2e88cc..ae474cf8d68 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -14,6 +14,9 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow issues bulk update
before_action :authorize_admin_issues!, only: [:bulk_update]
+ # Cross-reference merge requests
+ before_action :closed_by_merge_requests, only: [:show]
+
respond_to :html
def index
@@ -55,9 +58,9 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
- @participants = @issue.participants(current_user, @project)
+ @participants = @issue.participants(current_user)
@note = @project.notes.new(noteable: @issue)
- @notes = @issue.notes.inc_author.fresh
+ @notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
respond_with(@issue)
@@ -103,7 +106,7 @@ class Projects::IssuesController < Projects::ApplicationController
def bulk_update
result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
- redirect_to :back, notice: "#{result[:count]} issues updated"
+ redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
end
def toggle_subscription
@@ -112,6 +115,10 @@ class Projects::IssuesController < Projects::ApplicationController
render nothing: true
end
+ def closed_by_merge_requests
+ @closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
+ end
+
protected
def issue
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 7574842cd43..3f47f2ddb2c 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -31,6 +31,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
@merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
+ @merge_requests = @merge_requests.preload(:target_project)
respond_to do |format|
format.html
@@ -56,6 +57,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def diffs
@commit = @merge_request.last_commit
+ @first_commit = @merge_request.first_commit
+
@comments_allowed = @reply_allowed = true
@comments_target = {
noteable_type: 'MergeRequest',
@@ -89,7 +92,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits
- @commit = @merge_request.compare_commits.last
+ @commit = @merge_request.last_commit
+ @first_commit = @merge_request.first_commit
@diffs = @merge_request.compare_diffs
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
@@ -150,6 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return access_denied! unless @merge_request.can_be_merged_by?(current_user)
if @merge_request.mergeable?
+ @merge_request.update(merge_error: nil)
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = true
else
@@ -245,11 +250,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def define_show_vars
- @participants = @merge_request.participants(current_user, @project)
+ @participants = @merge_request.participants(current_user)
# Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request)
- @notes = @merge_request.mr_and_commit_notes.inc_author.fresh
+ @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh
@discussions = Note.discussions_from_notes(@notes)
@noteable = @merge_request
@@ -258,7 +263,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@commits = @merge_request.commits
@merge_request_diff = @merge_request.merge_request_diff
-
+
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 86f4a02a6e9..15506bd677a 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -75,11 +75,7 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def sort_issues
- @issues = @milestone.issues.where(id: params['sortable_issue'])
- @issues.each do |issue|
- issue.position = params['sortable_issue'].index(issue.id.to_s) + 1
- issue.save
- end
+ @milestone.sort_issues(params['sortable_issue'].map(&:to_i))
render json: { saved: true }
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 0f5d82ce133..88b949a27ab 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
- before_action :find_current_user_notes, except: [:destroy, :delete_attachment]
+ before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle]
def index
current_fetched_at = Time.now.to_i
@@ -25,7 +25,7 @@ class Projects::NotesController < Projects::ApplicationController
respond_to do |format|
format.json { render_note_json(@note) }
- format.html { redirect_to :back }
+ format.html { redirect_back_or_default }
end
end
@@ -34,7 +34,7 @@ class Projects::NotesController < Projects::ApplicationController
respond_to do |format|
format.json { render_note_json(@note) }
- format.html { redirect_to :back }
+ format.html { redirect_back_or_default }
end
end
@@ -58,6 +58,30 @@ class Projects::NotesController < Projects::ApplicationController
end
end
+ def award_toggle
+ noteable = if note_params[:noteable_type] == "issue"
+ project.issues.find(note_params[:noteable_id])
+ else
+ project.merge_requests.find(note_params[:noteable_id])
+ end
+
+ data = {
+ author: current_user,
+ is_award: true,
+ note: note_params[:note].gsub(":", '')
+ }
+
+ note = noteable.notes.find_by(data)
+
+ if note
+ note.destroy
+ else
+ Notes::CreateService.new(project, current_user, note_params).execute
+ end
+
+ render json: { ok: true }
+ end
+
private
def note
@@ -107,13 +131,25 @@ class Projects::NotesController < Projects::ApplicationController
end
def render_note_json(note)
- render json: {
- id: note.id,
- discussion_id: note.discussion_id,
- html: note_to_html(note),
- discussion_html: note_to_discussion_html(note),
- discussion_with_diff_html: note_to_discussion_with_diff_html(note)
- }
+ if note.valid?
+ render json: {
+ valid: true,
+ id: note.id,
+ discussion_id: note.discussion_id,
+ html: note_to_html(note),
+ award: note.is_award,
+ emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "",
+ note: note.note,
+ discussion_html: note_to_discussion_html(note),
+ discussion_with_diff_html: note_to_discussion_with_diff_html(note)
+ }
+ else
+ render json: {
+ valid: false,
+ award: note.is_award,
+ errors: note.errors
+ }
+ end
end
def authorize_admin_note!
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index cf73bc01c8f..07eb94e4f48 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -1,6 +1,6 @@
class Projects::ProjectMembersController < Projects::ApplicationController
# Authorize
- before_action :authorize_admin_project!, except: :leave
+ before_action :authorize_admin_project_member!, except: :leave
def index
@project_members = @project.project_members
@@ -29,10 +29,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_member = @project.project_members.new
end
- def new
- @project_member = @project.project_members.new
- end
-
def create
@project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
@@ -41,11 +37,17 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def update
@project_member = @project.project_members.find(params[:id])
+
+ return render_403 unless can?(current_user, :update_project_member, @project_member)
+
@project_member.update_attributes(member_params)
end
def destroy
@project_member = @project.project_members.find(params[:id])
+
+ return render_403 unless can?(current_user, :destroy_project_member, @project_member)
+
@project_member.destroy
respond_to do |format|
@@ -71,15 +73,22 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def leave
- if @project.namespace == current_user.namespace
- return redirect_to(:back, alert: 'You can not leave your own project. Transfer or delete the project.')
- end
+ @project_member = @project.project_members.find_by(user_id: current_user)
- @project.project_members.find_by(user_id: current_user).destroy
+ if can?(current_user, :destroy_project_member, @project_member)
+ @project_member.destroy
- respond_to do |format|
- format.html { redirect_to dashboard_projects_path }
- format.js { render nothing: true }
+ respond_to do |format|
+ format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
+ format.js { render nothing: true }
+ end
+ else
+ if current_user == @project.owner
+ message = 'You can not leave your own project. Transfer or delete the project.'
+ redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
+ else
+ render_403
+ end
end
end
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 5f6fbce795e..d5ee6ac8663 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -20,7 +20,7 @@ class Projects::RawController < Projects::ApplicationController
disposition: 'inline'
)
else
- not_found!
+ render_404
end
end
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 6080c849c8d..c4e18c17077 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -3,6 +3,7 @@ class Projects::RefsController < Projects::ApplicationController
include TreeHelper
before_action :require_non_empty_project
+ before_action :validate_ref_id
before_action :assign_ref_vars
before_action :authorize_download_code!
@@ -71,4 +72,10 @@ class Projects::RefsController < Projects::ApplicationController
format.js
end
end
+
+ private
+
+ def validate_ref_id
+ return not_found! if params[:id].present? && params[:id] !~ Gitlab::Regex.git_reference_regex
+ end
end
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
new file mode 100644
index 00000000000..0825a4311cb
--- /dev/null
+++ b/app/controllers/projects/releases_controller.rb
@@ -0,0 +1,31 @@
+class Projects::ReleasesController < Projects::ApplicationController
+ # Authorize
+ before_action :require_non_empty_project
+ before_action :authorize_download_code!
+ before_action :authorize_push_code!
+ before_action :tag
+ before_action :release
+
+ def edit
+ end
+
+ def update
+ release.update_attributes(release_params)
+
+ redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
+ end
+
+ private
+
+ def tag
+ @tag ||= @repository.find_tag(params[:tag_id])
+ end
+
+ def release
+ @release ||= @project.releases.find_or_initialize_by(tag: @tag.name)
+ end
+
+ def release_params
+ params.require(:release).permit(:description)
+ end
+end
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index c4a5e2d6359..ba9aea1c165 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -11,18 +11,9 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def archive
- begin
- file_path = ArchiveRepositoryService.new(@project, params[:ref], params[:format]).execute
- rescue
- return head :not_found
- end
-
- if file_path
- # Send file to user
- response.headers["Content-Length"] = File.open(file_path).size.to_s
- send_file file_path
- else
- redirect_to request.fullpath
- end
+ render json: ArchiveRepositoryService.new(@project, params[:ref], params[:format]).execute
+ rescue => ex
+ logger.error("#{self.class.name}: #{ex}")
+ return git_not_found!
end
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
new file mode 100644
index 00000000000..bfbcf2567f3
--- /dev/null
+++ b/app/controllers/projects/runners_controller.rb
@@ -0,0 +1,64 @@
+class Projects::RunnersController < Projects::ApplicationController
+ before_action :ci_project
+ before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
+ before_action :authorize_admin_project!
+
+ layout 'project_settings'
+
+ def index
+ @runners = @ci_project.runners.ordered
+ @specific_runners = current_user.ci_authorized_runners.
+ where.not(id: @ci_project.runners).
+ ordered.page(params[:page]).per(20)
+ @shared_runners = Ci::Runner.shared.active
+ @shared_runners_count = @shared_runners.count(:all)
+ end
+
+ def edit
+ end
+
+ def update
+ if @runner.update_attributes(runner_params)
+ redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
+ else
+ redirect_to runner_path(@runner), alert: 'Runner was not updated.'
+ end
+ end
+
+ def destroy
+ if @runner.only_for?(@ci_project)
+ @runner.destroy
+ end
+
+ redirect_to runners_path(@project)
+ end
+
+ def resume
+ if @runner.update_attributes(active: true)
+ redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
+ else
+ redirect_to runner_path(@runner), alert: 'Runner was not updated.'
+ end
+ end
+
+ def pause
+ if @runner.update_attributes(active: false)
+ redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
+ else
+ redirect_to runner_path(@runner), alert: 'Runner was not updated.'
+ end
+ end
+
+ def show
+ end
+
+ protected
+
+ def set_runner
+ @runner ||= @ci_project.runners.find(params[:id])
+ end
+
+ def runner_params
+ params.require(:runner).permit(:description, :tag_list, :active)
+ end
+end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 3a22ed832ac..42dbb497e01 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -9,6 +9,10 @@ class Projects::ServicesController < Projects::ApplicationController
:note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url,
:notify, :color,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification]
+
+ # Parameters to ignore if no value is specified
+ FILTER_BLANK_PARAMS = [:password]
+
# Authorize
before_action :authorize_admin_project!
before_action :service, only: [:edit, :update, :test]
@@ -48,7 +52,7 @@ class Projects::ServicesController < Projects::ApplicationController
message = { alert: error_message }
end
- redirect_to :back, message
+ redirect_back_or_default(options: message)
end
private
@@ -58,6 +62,10 @@ class Projects::ServicesController < Projects::ApplicationController
end
def service_params
- params.require(:service).permit(ALLOWED_PARAMS)
+ service_params = params.require(:service).permit(ALLOWED_PARAMS)
+ FILTER_BLANK_PARAMS.each do |param|
+ service_params.delete(param) if service_params[param].blank?
+ end
+ service_params
end
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index b07a2a8db2f..2104c7a7a71 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -21,6 +21,7 @@ class Projects::SnippetsController < Projects::ApplicationController
filter: :by_project,
project: @project
})
+ @snippets = @snippets.page(params[:page]).per(PER_PAGE)
end
def new
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index f565fbbbbc3..280fe12cc7c 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -2,21 +2,29 @@ class Projects::TagsController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :authorize_push_code!, only: [:create]
+ before_action :authorize_push_code!, only: [:new, :create]
before_action :authorize_admin_project!, only: [:destroy]
def index
sorted = VersionSorter.rsort(@repository.tag_names)
@tags = Kaminari.paginate_array(sorted).page(params[:page]).per(PER_PAGE)
+ @releases = project.releases.where(tag: @tags)
+ end
+
+ def show
+ @tag = @repository.find_tag(params[:id])
+ @release = @project.releases.find_or_initialize_by(tag: @tag.name)
+ @commit = @repository.commit(@tag.target)
end
def create
result = CreateTagService.new(@project, current_user).
- execute(params[:tag_name], params[:ref], params[:message])
+ execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
if result[:status] == :success
@tag = result[:tag]
- redirect_to namespace_project_tags_path(@project.namespace, @project)
+
+ redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
else
@error = result[:message]
render action: 'new'
@@ -26,12 +34,6 @@ class Projects::TagsController < Projects::ApplicationController
def destroy
DeleteTagService.new(project, current_user).execute(params[:id])
- respond_to do |format|
- format.html do
- redirect_to namespace_project_tags_path(@project.namespace,
- @project)
- end
- format.js
- end
+ redirect_to namespace_project_tags_path(@project.namespace, @project)
end
end
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 92e4bc16d9d..8f272ad1281 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -1,13 +1,17 @@
# Controller for viewing a repository's file structure
class Projects::TreeController < Projects::ApplicationController
include ExtractsPath
+ include CreatesMergeRequestForCommit
+ include ActionView::Helpers::SanitizeHelper
before_action :require_non_empty_project, except: [:new, :create]
before_action :assign_ref_vars
+ before_action :assign_dir_vars, only: [:create_dir]
before_action :authorize_download_code!
+ before_action :authorize_push_code!, only: [:create_dir]
def show
- return not_found! unless @repository.commit(@ref)
+ return render_404 unless @repository.commit(@ref)
if tree.entries.empty?
if @repository.blob_at(@commit.id, @path)
@@ -16,7 +20,7 @@ class Projects::TreeController < Projects::ApplicationController
File.join(@ref, @path))
) and return
elsif @path.present?
- return not_found!
+ return render_404
end
end
@@ -26,4 +30,48 @@ class Projects::TreeController < Projects::ApplicationController
format.js { no_cache_headers }
end
end
+
+ def create_dir
+ return render_404 unless @commit_params.values.all?
+
+ begin
+ result = Files::CreateDirService.new(@project, current_user, @commit_params).execute
+ message = result[:message]
+ rescue => e
+ message = e.to_s
+ end
+
+ if result && result[:status] == :success
+ flash[:notice] = "The directory has been successfully created"
+ respond_to do |format|
+ format.html { redirect_to after_create_dir_path }
+ end
+ else
+ flash[:alert] = message
+ respond_to do |format|
+ format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, @new_branch) }
+ end
+ end
+ end
+
+ private
+
+ def assign_dir_vars
+ @new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref
+ @dir_name = File.join(@path, params[:dir_name])
+ @commit_params = {
+ file_path: @dir_name,
+ current_branch: @ref,
+ target_branch: @new_branch,
+ commit_message: params[:commit_message],
+ }
+ end
+
+ def after_create_dir_path
+ if create_merge_request?
+ new_merge_request_path
+ else
+ namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name))
+ end
+ end
end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
new file mode 100644
index 00000000000..782ebd01b05
--- /dev/null
+++ b/app/controllers/projects/triggers_controller.rb
@@ -0,0 +1,35 @@
+class Projects::TriggersController < Projects::ApplicationController
+ before_action :ci_project
+ before_action :authorize_admin_project!
+
+ layout 'project_settings'
+
+ def index
+ @triggers = @ci_project.triggers
+ @trigger = Ci::Trigger.new
+ end
+
+ def create
+ @trigger = @ci_project.triggers.new
+ @trigger.save
+
+ if @trigger.valid?
+ redirect_to namespace_project_triggers_path(@project.namespace, @project)
+ else
+ @triggers = @ci_project.triggers.select(&:persisted?)
+ render :index
+ end
+ end
+
+ def destroy
+ trigger.destroy
+
+ redirect_to namespace_project_triggers_path(@project.namespace, @project)
+ end
+
+ private
+
+ def trigger
+ @trigger ||= @ci_project.triggers.find(params[:id])
+ end
+end
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index 71ecc20dd95..e1fe7ea2114 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -20,7 +20,7 @@ class Projects::UploadsController < Projects::ApplicationController
end
def show
- return not_found! if uploader.nil? || !uploader.file.exists?
+ return render_404 if uploader.nil? || !uploader.file.exists?
disposition = uploader.image? ? 'inline' : 'attachment'
send_file uploader.file.path, disposition: disposition
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
new file mode 100644
index 00000000000..d6561a45a70
--- /dev/null
+++ b/app/controllers/projects/variables_controller.rb
@@ -0,0 +1,25 @@
+class Projects::VariablesController < Projects::ApplicationController
+ before_action :ci_project
+ before_action :authorize_admin_project!
+
+ layout 'project_settings'
+
+ def show
+ end
+
+ def update
+ if ci_project.update_attributes(project_params)
+ Ci::EventService.new.change_project_settings(current_user, ci_project)
+
+ redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.'
+ else
+ render action: 'show'
+ end
+ end
+
+ private
+
+ def project_params
+ params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] })
+ end
+end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 51c26a6a465..88fccfed509 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -98,7 +98,7 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
- rescue ProjectWiki::CouldNotCreateWikiError => ex
+ rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
return false
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 213c2a7173b..10c75370d7b 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,11 +1,14 @@
class ProjectsController < ApplicationController
- prepend_before_filter :render_go_import, only: [:show]
+ 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, :destroy, :transfer, :archive, :unarchive]
+ before_action :authorize_admin_project!, only: [:edit, :update]
before_action :event_filter, only: [:show, :activity]
layout :determine_layout
@@ -56,6 +59,8 @@ class ProjectsController < ApplicationController
end
def transfer
+ return access_denied! unless can?(current_user, :change_namespace, @project)
+
namespace = Namespace.find_by(id: params[:new_namespace_id])
::Projects::TransferService.new(project, current_user).execute(namespace)
@@ -64,6 +69,14 @@ class ProjectsController < ApplicationController
end
end
+ def remove_fork
+ return access_denied! unless can?(current_user, :remove_fork_project, @project)
+
+ if @project.unlink_fork
+ flash[:notice] = 'The fork relationship has been removed.'
+ end
+ end
+
def activity
respond_to do |format|
format.html
@@ -87,7 +100,7 @@ class ProjectsController < ApplicationController
render 'projects/empty'
else
if current_user
- @membership = @project.project_member_by_id(current_user.id)
+ @membership = @project.team.find_member(current_user.id)
end
render :show
@@ -110,11 +123,7 @@ class ProjectsController < ApplicationController
::Projects::DestroyService.new(@project, current_user, {}).execute
flash[:alert] = "Project '#{@project.name}' was deleted."
- if request.referer.include?('/admin')
- redirect_to admin_namespaces_projects_path
- else
- redirect_to dashboard_projects_path
- end
+ redirect_to dashboard_projects_path
rescue Projects::DestroyService::DestroyError => ex
redirect_to edit_project_path(@project), alert: ex.message
end
@@ -139,6 +148,7 @@ class ProjectsController < ApplicationController
def archive
return access_denied! unless can?(current_user, :archive_project, @project)
+
@project.archive!
respond_to do |format|
@@ -148,6 +158,7 @@ class ProjectsController < ApplicationController
def unarchive
return access_denied! unless can?(current_user, :archive_project, @project)
+
@project.unarchive!
respond_to do |format|
@@ -201,7 +212,8 @@ class ProjectsController < ApplicationController
params.require(:project).permit(
:name, :path, :description, :issues_tracker, :tag_list,
:issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch,
- :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar
+ :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
+ :builds_enabled
)
end
@@ -225,4 +237,14 @@ class ProjectsController < ApplicationController
render "go_import", layout: false
end
+
+ def repo_exists?
+ project.repository_exists? && !project.empty_repo?
+ end
+
+ # Override get_id from ExtractsPath, which returns the branch and file path
+ # for the blob/tree, which in this case is just the root of the default branch.
+ def get_id
+ project.repository.root_ref
+ end
end
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index 54171ff67c5..ad04c646e1b 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -22,6 +22,10 @@ class RootController < Dashboard::ProjectsController
when 'stars'
flash.keep
redirect_to starred_dashboard_projects_path
+ when 'project_activity'
+ redirect_to activity_dashboard_path
+ when 'starred_project_activity'
+ redirect_to activity_dashboard_path(filter: 'starred')
else
return
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index eb0408a95e5..9bb42ec86b3 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -23,8 +23,8 @@ class SearchController < ApplicationController
@search_results =
if @project
- unless %w(blobs notes issues merge_requests milestones wiki_blobs).
- include?(@scope)
+ unless %w(blobs notes issues merge_requests milestones wiki_blobs
+ commits).include?(@scope)
@scope = 'blobs'
end
diff --git a/app/controllers/sherlock/application_controller.rb b/app/controllers/sherlock/application_controller.rb
new file mode 100644
index 00000000000..682ca5e3821
--- /dev/null
+++ b/app/controllers/sherlock/application_controller.rb
@@ -0,0 +1,12 @@
+module Sherlock
+ class ApplicationController < ::ApplicationController
+ before_action :find_transaction
+
+ def find_transaction
+ if params[:transaction_id]
+ @transaction = Gitlab::Sherlock.collection.
+ find_transaction(params[:transaction_id])
+ end
+ end
+ end
+end
diff --git a/app/controllers/sherlock/file_samples_controller.rb b/app/controllers/sherlock/file_samples_controller.rb
new file mode 100644
index 00000000000..0c3bc100106
--- /dev/null
+++ b/app/controllers/sherlock/file_samples_controller.rb
@@ -0,0 +1,7 @@
+module Sherlock
+ class FileSamplesController < Sherlock::ApplicationController
+ def show
+ @file_sample = @transaction.find_file_sample(params[:id])
+ end
+ end
+end
diff --git a/app/controllers/sherlock/queries_controller.rb b/app/controllers/sherlock/queries_controller.rb
new file mode 100644
index 00000000000..63b26aab1a4
--- /dev/null
+++ b/app/controllers/sherlock/queries_controller.rb
@@ -0,0 +1,7 @@
+module Sherlock
+ class QueriesController < Sherlock::ApplicationController
+ def show
+ @query = @transaction.find_query(params[:id])
+ end
+ end
+end
diff --git a/app/controllers/sherlock/transactions_controller.rb b/app/controllers/sherlock/transactions_controller.rb
new file mode 100644
index 00000000000..ccc739da879
--- /dev/null
+++ b/app/controllers/sherlock/transactions_controller.rb
@@ -0,0 +1,19 @@
+module Sherlock
+ class TransactionsController < Sherlock::ApplicationController
+ def index
+ @transactions = Gitlab::Sherlock.collection.newest_first
+ end
+
+ def show
+ @transaction = Gitlab::Sherlock.collection.find_transaction(params[:id])
+
+ render_404 unless @transaction
+ end
+
+ def destroy_all
+ Gitlab::Sherlock.collection.clear
+
+ redirect_to(:back)
+ end
+ end
+end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 9f9f9a92f11..c72df73af46 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,6 +1,9 @@
class SnippetsController < ApplicationController
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
+ # Allow read snippet
+ before_action :authorize_read_snippet!, only: [:show, :raw]
+
# Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update]
@@ -79,10 +82,14 @@ class SnippetsController < ApplicationController
[Snippet::PUBLIC, Snippet::INTERNAL]).
find(params[:id])
else
- PersonalSnippet.are_public.find(params[:id])
+ PersonalSnippet.find(params[:id])
end
end
+ def authorize_read_snippet!
+ authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
+ end
+
def authorize_update_snippet!
return render_404 unless can?(current_user, :update_personal_snippet, @snippet)
end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 28536e359e5..868b05929d7 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -10,7 +10,7 @@ class UploadsController < ApplicationController
end
unless uploader.file && uploader.file.exists?
- return not_found!
+ return render_404
end
disposition = uploader.image? ? 'inline' : 'attachment'
@@ -21,7 +21,7 @@ class UploadsController < ApplicationController
def find_model
unless upload_model && upload_mount
- return not_found!
+ return render_404
end
@model = upload_model.find(params[:id])
@@ -44,7 +44,7 @@ class UploadsController < ApplicationController
return if authorized
if current_user
- not_found!
+ render_404
else
authenticate_user!
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 1484356a7f4..30cb869eb2a 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -3,14 +3,11 @@ class UsersController < ApplicationController
before_action :set_user
def show
- @contributed_projects = contributed_projects.joined(@user).
- reject(&:forked?)
+ @contributed_projects = contributed_projects.joined(@user).reject(&:forked?)
- @projects = @user.personal_projects.
- where(id: authorized_projects_ids).includes(:namespace)
+ @projects = PersonalProjectsFinder.new(@user).execute(current_user)
- # Collect only groups common for both users
- @groups = @user.groups & GroupsFinder.new.execute(current_user)
+ @groups = JoinedGroupsFinder.new(@user).execute(current_user)
respond_to do |format|
format.html
@@ -53,16 +50,8 @@ class UsersController < ApplicationController
@user = User.find_by_username!(params[:username])
end
- def authorized_projects_ids
- # Projects user can view
- @authorized_projects_ids ||=
- ProjectsFinder.new.execute(current_user).pluck(:id)
- end
-
def contributed_projects
- @contributed_projects = Project.
- where(id: authorized_projects_ids & @user.contributed_projects_ids).
- includes(:namespace)
+ ContributedProjectsFinder.new(@user).execute(current_user)
end
def contributions_calendar
@@ -73,9 +62,13 @@ class UsersController < ApplicationController
def load_events
# Get user activity feed for projects common for both users
@events = @user.recent_events.
- where(project_id: authorized_projects_ids).
- with_associations
+ merge(projects_for_current_user).
+ references(:project).
+ with_associations.
+ limit_recent(20, params[:offset])
+ end
- @events = @events.limit(20).offset(params[:offset] || 0)
+ def projects_for_current_user
+ ProjectsFinder.new.execute(current_user)
end
end
diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb
new file mode 100644
index 00000000000..0209649b017
--- /dev/null
+++ b/app/finders/contributed_projects_finder.rb
@@ -0,0 +1,37 @@
+class ContributedProjectsFinder
+ def initialize(user)
+ @user = user
+ end
+
+ # Finds the projects "@user" contributed to, limited to either public projects
+ # or projects visible to the given user.
+ #
+ # current_user - When given the list of the projects is limited to those only
+ # visible by this user.
+ #
+ # Returns an ActiveRecord::Relation.
+ def execute(current_user = nil)
+ if current_user
+ relation = projects_visible_to_user(current_user)
+ else
+ relation = public_projects
+ end
+
+ relation.includes(:namespace).order_id_desc
+ end
+
+ private
+
+ def projects_visible_to_user(current_user)
+ authorized = @user.contributed_projects.visible_to_user(current_user)
+
+ union = Gitlab::SQL::Union.
+ new([authorized.select(:id), public_projects.select(:id)])
+
+ Project.where("projects.id IN (#{union.to_sql})")
+ end
+
+ def public_projects
+ @user.contributed_projects.public_only
+ end
+end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index d3597ef0901..91cb0f228f0 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -1,38 +1,44 @@
class GroupsFinder
- def execute(current_user, options = {})
- all_groups(current_user)
+ # 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
- def all_groups(current_user)
- if current_user
- if current_user.authorized_groups.any?
- # User has access to groups
- #
- # Return only:
- # groups with public projects
- # groups with internal projects
- # groups with joined projects
- #
- group_ids = Project.public_and_internal_only.pluck(:namespace_id) +
- current_user.authorized_groups.pluck(:id)
- Group.where(id: group_ids)
- else
- # User has no group membership
- #
- # Return only:
- # groups with public projects
- # groups with internal projects
- #
- Group.where(id: Project.public_and_internal_only.pluck(:namespace_id))
- end
- else
- # Not authenticated
- #
- # Return only:
- # groups with public projects
- Group.where(id: Project.public_only.pluck(:namespace_id))
- end
+ # 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 ab89aa2c53a..3d5e8b6fbe7 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -39,7 +39,7 @@ class IssuableFinder
items = by_assignee(items)
items = by_author(items)
items = by_label(items)
- items = sort(items)
+ sort(items)
end
def group
@@ -53,15 +53,36 @@ class IssuableFinder
end
end
+ def project?
+ params[:project_id].present?
+ end
+
def project
return @project if defined?(@project)
- @project =
- if params[:project_id].present?
- Project.find(params[:project_id])
- else
- nil
+ 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 projects
+ return @projects if defined?(@projects)
+
+ if project?
+ @projects = project
+ elsif current_user && params[:authorized_only].presence && !current_user_related?
+ @projects = current_user.authorized_projects
+ else
+ @projects = ProjectsFinder.new.execute(current_user)
+ end
end
def search
@@ -72,17 +93,31 @@ class IssuableFinder
params[:milestone_title].present?
end
+ def filter_by_no_milestone?
+ milestones? && params[:milestone_title] == Milestone::None.title
+ end
+
def milestones
return @milestones if defined?(@milestones)
@milestones =
- if milestones? && params[:milestone_title] != Milestone::None.title
- Milestone.where(title: params[:milestone_title])
+ if milestones?
+ scope = Milestone.where(project_id: projects)
+
+ scope.where(title: params[:milestone_title])
else
nil
end
end
+ def labels?
+ params[:label_name].present?
+ end
+
+ def filter_by_no_label?
+ labels? && params[:label_name] == Label::None.title
+ end
+
def assignee?
params[:assignee_id].present?
end
@@ -116,19 +151,7 @@ class IssuableFinder
private
def init_collection
- table_name = klass.table_name
-
- if project
- if Ability.abilities.allowed?(current_user, :read_project, project)
- project.send(table_name)
- else
- []
- end
- elsif current_user && params[:authorized_only].presence && !current_user_related?
- klass.of_projects(current_user.authorized_projects).references(:project)
- else
- klass.of_projects(ProjectsFinder.new.execute(current_user)).references(:project)
- end
+ klass.all
end
def by_scope(items)
@@ -166,7 +189,14 @@ class IssuableFinder
end
def by_project(items)
- items = items.of_projects(project.id) if project
+ items =
+ if project?
+ items.of_projects(projects).references_project
+ elsif projects
+ items.merge(projects.reorder(nil)).join_project
+ else
+ items.none
+ end
items
end
@@ -178,15 +208,9 @@ class IssuableFinder
end
def sort(items)
- items.sort(params[:sort])
- end
-
- def by_milestone(items)
- if milestones?
- items = items.where(milestone_id: milestones.try(:pluck, :id))
- end
-
- items
+ # Ensure we always have an explicit sort order (instead of inheriting
+ # multiple orders when combining ActiveRecord::Relation objects).
+ params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
end
def by_assignee(items)
@@ -205,15 +229,37 @@ class IssuableFinder
items
end
+ def by_milestone(items)
+ if milestones?
+ if filter_by_no_milestone?
+ items = items.where(milestone_id: [-1, nil])
+ else
+ items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
+
+ if projects
+ items = items.where(milestones: { project_id: projects })
+ end
+ end
+ end
+
+ items
+ end
+
def by_label(items)
- if params[:label_name].present?
- label_names = params[:label_name].split(",")
+ 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 })
+ else
+ label_names = params[:label_name].split(",")
- item_ids = LabelLink.joins(:label).
- where('labels.title in (?)', label_names).
- where(target_type: klass.name).pluck(:target_id)
+ items = items.joins(:labels).where(labels: { title: label_names })
- items = items.where(id: item_ids)
+ if projects
+ items = items.where(labels: { project_id: projects })
+ end
+ end
end
items
diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb
new file mode 100644
index 00000000000..e7523136fea
--- /dev/null
+++ b/app/finders/joined_groups_finder.rb
@@ -0,0 +1,49 @@
+# 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/milestones_finder.rb b/app/finders/milestones_finder.rb
new file mode 100644
index 00000000000..630c73c2a94
--- /dev/null
+++ b/app/finders/milestones_finder.rb
@@ -0,0 +1,12 @@
+class MilestonesFinder
+ def execute(projects, params)
+ milestones = Milestone.of_projects(projects)
+ milestones = milestones.reorder("due_date ASC")
+
+ case params[:state]
+ when 'closed' then milestones.closed
+ when 'all' then milestones
+ else milestones.active
+ end
+ end
+end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index ab252821b52..fa4c635f55c 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -12,9 +12,9 @@ class NotesFinder
when "commit"
project.notes.for_commit_id(target_id).not_inline
when "issue"
- project.issues.find(target_id).notes.inc_author
+ project.issues.find(target_id).notes.nonawards.inc_author
when "merge_request"
- project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
+ project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author
when "snippet", "project_snippet"
project.snippets.find(target_id).notes
else
diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb
new file mode 100644
index 00000000000..a61ffa22990
--- /dev/null
+++ b/app/finders/personal_projects_finder.rb
@@ -0,0 +1,41 @@
+class PersonalProjectsFinder
+ def initialize(user)
+ @user = user
+ end
+
+ # Finds the projects belonging to the user in "@user", limited to either
+ # public projects or projects visible to the given user.
+ #
+ # current_user - When given the list of projects is limited to those only
+ # visible by this user.
+ #
+ # Returns an ActiveRecord::Relation.
+ def execute(current_user = nil)
+ if current_user
+ relation = projects_visible_to_user(current_user)
+ else
+ relation = public_projects
+ end
+
+ relation.includes(:namespace).order_id_desc
+ end
+
+ private
+
+ def projects_visible_to_user(current_user)
+ authorized = @user.personal_projects.visible_to_user(current_user)
+
+ union = Gitlab::SQL::Union.
+ new([authorized.select(:id), public_and_internal_projects.select(:id)])
+
+ Project.where("projects.id IN (#{union.to_sql})")
+ end
+
+ def public_projects
+ @user.personal_projects.public_only
+ end
+
+ def public_and_internal_projects
+ @user.personal_projects.public_and_internal_only
+ end
+end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index c81bb51583a..3b4e0362e04 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -1,11 +1,39 @@
class ProjectsFinder
- def execute(current_user, options = {})
+ # Returns all projects, optionally including group projects a user has access
+ # to.
+ #
+ # ## Examples
+ #
+ # Retrieving all public projects:
+ #
+ # ProjectsFinder.new.execute
+ #
+ # Retrieving all public/internal projects and those the given user has access
+ # to:
+ #
+ # ProjectsFinder.new.execute(some_user)
+ #
+ # Retrieving all public/internal projects as well as the group's projects the
+ # user has access to:
+ #
+ # ProjectsFinder.new.execute(some_user, group: some_group)
+ #
+ # Returns an ActiveRecord::Relation.
+ def execute(current_user = nil, options = {})
group = options[:group]
if group
- group_projects(current_user, group)
+ segments = group_projects(current_user, group)
else
- all_projects(current_user)
+ segments = all_projects(current_user)
+ end
+
+ if segments.length > 1
+ union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) })
+
+ Project.where("projects.id IN (#{union.to_sql})")
+ else
+ segments.first
end
end
@@ -13,77 +41,36 @@ class ProjectsFinder
def group_projects(current_user, group)
if current_user
- if group.users.include?(current_user)
- # User is group member
- #
- # Return ALL group projects
- group.projects
- else
- projects_members = ProjectMember.in_projects(group.projects).
- with_user(current_user)
-
- if projects_members.any?
- # User is a project member
- #
- # Return only:
- # public projects
- # internal projects
- # joined projects
- #
- group.projects.where(
- "projects.id IN (?) OR projects.visibility_level IN (?)",
- projects_members.pluck(:source_id),
- Project.public_and_internal_levels
- )
- else
- # User has no access to group or group projects
- #
- # Return only:
- # public projects
- # internal projects
- #
- group.projects.public_and_internal_only
- end
- end
+ [
+ group_projects_for_user(current_user, group),
+ group.projects.public_and_internal_only
+ ]
else
- # Not authenticated
- #
- # Return only:
- # public projects
- group.projects.public_only
+ [group.projects.public_only]
end
end
def all_projects(current_user)
if current_user
- if current_user.authorized_projects.any?
- # User has access to private projects
- #
- # Return only:
- # public projects
- # internal projects
- # joined projects
- #
- Project.where(
- "projects.id IN (?) OR projects.visibility_level IN (?)",
- current_user.authorized_projects.pluck(:id),
- Project.public_and_internal_levels
- )
- else
- # User has no access to private projects
- #
- # Return only:
- # public projects
- # internal projects
- #
- Project.public_and_internal_only
- end
+ [current_user.authorized_projects, public_and_internal_projects]
else
- # Not authenticated
- #
- # Return only:
- # public projects
- Project.public_only
+ [Project.public_only]
end
end
+
+ def group_projects_for_user(current_user, group)
+ if group.users.include?(current_user)
+ group.projects
+ else
+ group.projects.visible_to_user(current_user)
+ end
+ end
+
+ def public_projects
+ Project.unscoped.public_only
+ end
+
+ def public_and_internal_projects
+ Project.unscoped.public_and_internal_only
+ end
end
diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb
index 9ea342cb26d..81a12403801 100644
--- a/app/finders/trending_projects_finder.rb
+++ b/app/finders/trending_projects_finder.rb
@@ -1,13 +1,6 @@
class TrendingProjectsFinder
- def execute(current_user, start_date = nil)
- start_date ||= Date.today - 1.month
-
- projects = projects_for(current_user)
-
- # Determine trending projects based on comments count
- # for period of time - ex. month
- projects.joins(:notes).where('notes.created_at > ?', start_date).
- group("projects.id").reorder("count(notes.id) DESC")
+ def execute(current_user, start_date = 1.month.ago)
+ projects_for(current_user).trending(start_date)
end
private
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 14df8d4cbd7..c5820bf4c50 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -16,6 +16,6 @@ module AppearancesHelper
end
def brand_header_logo
- image_tag 'logo.svg'
+ render 'shared/logo.svg'
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 39ab83ccf12..21f962df206 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -35,7 +35,7 @@ module ApplicationHelper
def project_icon(project_id, options = {})
project =
if project_id.is_a?(Project)
- project = project_id
+ project_id
else
Project.find_with_namespace(project_id)
end
@@ -68,18 +68,22 @@ module ApplicationHelper
end
end
- def avatar_icon(user_email = '', size = nil)
- user = User.find_by(email: user_email)
+ def avatar_icon(user_or_email = nil, size = nil, scale = 2)
+ if user_or_email.is_a?(User)
+ user = user_or_email
+ else
+ user = User.find_by(email: user_or_email)
+ end
if user
user.avatar_url(size) || default_avatar
else
- gravatar_icon(user_email, size)
+ gravatar_icon(user_or_email, size, scale)
end
end
- def gravatar_icon(user_email = '', size = nil)
- GravatarService.new.execute(user_email, size) ||
+ def gravatar_icon(user_email = '', size = nil, scale = 2)
+ GravatarService.new.execute(user_email, size, scale) ||
default_avatar
end
@@ -205,7 +209,7 @@ module ApplicationHelper
title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
data: { toggle: 'tooltip', placement: placement, container: 'body' }
- element += javascript_tag "$('.js-timeago').timeago()" unless skip_js
+ element += javascript_tag "$('.js-timeago').last().timeago()" unless skip_js
element
end
@@ -314,4 +318,8 @@ module ApplicationHelper
html.html_safe
end
+
+ def truncate_first_line(message, length = 50)
+ truncate(message.each_line.first.chomp, length: length) if message
+ end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index ce7e9b1db87..2c81ea1623c 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,6 +1,6 @@
module AuthHelper
- PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze
- FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze
+ PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook).freeze
+ FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
Gitlab.config.ldap.enabled
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 77d99140c43..df5f5fae23c 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -60,7 +60,7 @@ module BlobHelper
if Gitlab::MarkupHelper.previewable?(filename)
'Preview'
else
- 'Preview changes'
+ 'Preview Changes'
end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
new file mode 100644
index 00000000000..313b6dde910
--- /dev/null
+++ b/app/helpers/button_helper.rb
@@ -0,0 +1,58 @@
+module ButtonHelper
+ # Output a "Copy to Clipboard" button
+ #
+ # data - Data attributes passed to `content_tag`
+ #
+ # Examples:
+ #
+ # # Define the clipboard's text
+ # clipboard_button(clipboard_text: "Foo")
+ # # => "<button class='...' data-clipboard-text='Foo'>...</button>"
+ #
+ # # Define the target element
+ # clipboard_button(clipboard_target: "#foo")
+ # # => "<button class='...' data-clipboard-target='#foo'>...</button>"
+ #
+ # See http://clipboardjs.com/#usage
+ def clipboard_button(data = {})
+ content_tag :button,
+ icon('clipboard'),
+ class: 'btn btn-xs btn-clipboard',
+ data: data,
+ type: :button
+ end
+
+ def http_clone_button(project)
+ klass = 'btn js-protocol-switch'
+ klass << ' active' if default_clone_protocol == 'http'
+ klass << ' has_tooltip' if current_user.try(:require_password?)
+
+ protocol = gitlab_config.protocol.upcase
+
+ content_tag :button, protocol,
+ class: klass,
+ data: {
+ clone: project.http_url_to_repo,
+ container: 'body',
+ html: 'true',
+ title: "Set a password on your account<br>to pull or push via #{protocol}"
+ },
+ type: :button
+ end
+
+ def ssh_clone_button(project)
+ klass = 'btn js-protocol-switch'
+ klass << ' active' if default_clone_protocol == 'ssh'
+ klass << ' has_tooltip' if current_user.try(:require_ssh_key?)
+
+ content_tag :button, 'SSH',
+ class: klass,
+ data: {
+ clone: project.ssh_url_to_repo,
+ container: 'body',
+ html: 'true',
+ title: 'Add an SSH key to your profile<br>to pull or push via SSH.'
+ },
+ type: :button
+ end
+end
diff --git a/app/helpers/ci/application_helper.rb b/app/helpers/ci/application_helper.rb
deleted file mode 100644
index 9fe6282bb81..00000000000
--- a/app/helpers/ci/application_helper.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-module Ci
- module ApplicationHelper
- def loader_html
- image_tag 'ci/loader.gif', alt: 'Loading'
- end
-
- def date_from_to(from, to)
- "#{from.to_s(:short)} - #{to.to_s(:short)}"
- end
-
- def duration_in_words(finished_at, started_at)
- if finished_at && started_at
- interval_in_seconds = finished_at.to_i - started_at.to_i
- elsif started_at
- interval_in_seconds = Time.now.to_i - started_at.to_i
- end
-
- time_interval_in_words(interval_in_seconds)
- end
-
- def time_interval_in_words(interval_in_seconds)
- minutes = interval_in_seconds / 60
- seconds = interval_in_seconds - minutes * 60
-
- if minutes >= 1
- "#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}"
- else
- "#{pluralize(seconds, "second")}"
- end
- end
-
- def ci_icon_for_status(status)
- icon_name =
- case status
- when 'success'
- 'check-square'
- when 'failed'
- 'close'
- when 'running', 'pending'
- 'clock-o'
- else
- 'circle'
- end
-
- icon(icon_name)
- end
-
- def ci_status_with_icon(status)
- content_tag :span, class: "ci-status ci-#{status}" do
- ci_icon_for_status(status) + '&nbsp;'.html_safe + status
- end
- end
- end
-end
diff --git a/app/helpers/ci/builds_helper.rb b/app/helpers/ci/builds_helper.rb
deleted file mode 100644
index 5d6e785d951..00000000000
--- a/app/helpers/ci/builds_helper.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Ci
- module BuildsHelper
- def build_ref_link build
- gitlab_ref_link build.project, build.ref
- end
-
- def build_compare_link build
- gitlab_compare_link build.project, build.commit.short_before_sha, build.short_sha
- end
-
- def build_commit_link build
- gitlab_commit_link build.project, build.short_sha
- end
-
- def build_url(build)
- ci_project_build_url(build.project, build)
- end
- end
-end
diff --git a/app/helpers/ci/commits_helper.rb b/app/helpers/ci/commits_helper.rb
deleted file mode 100644
index 9069aed5b4d..00000000000
--- a/app/helpers/ci/commits_helper.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module Ci
- module CommitsHelper
- def ci_commit_path(commit)
- ci_project_ref_commits_path(commit.project, commit.ref, commit.sha)
- end
-
- def commit_link(commit)
- link_to(commit.short_sha, ci_commit_path(commit))
- end
-
- def truncate_first_line(message, length = 50)
- truncate(message.each_line.first.chomp, length: length) if message
- end
-
- def ci_commit_title(commit)
- content_tag :span do
- link_to(
- simple_sanitize(commit.project.name), ci_project_path(commit.project)
- ) + ' @ ' +
- gitlab_commit_link(@project, @commit.sha)
- end
- end
- end
-end
diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb
index 2b89a0ce93e..e34c8be1dfc 100644
--- a/app/helpers/ci/gitlab_helper.rb
+++ b/app/helpers/ci/gitlab_helper.rb
@@ -4,32 +4,13 @@ module Ci
{ :"data-no-turbolink" => "data-no-turbolink" }
end
- def gitlab_ref_link project, ref
- gitlab_url = project.gitlab_url.dup
- gitlab_url << "/commits/#{ref}"
- link_to ref, gitlab_url, no_turbolink
- end
-
- def gitlab_compare_link project, before, after
- gitlab_url = project.gitlab_url.dup
- gitlab_url << "/compare/#{before}...#{after}"
-
- link_to "#{before}...#{after}", gitlab_url, no_turbolink
- end
-
- def gitlab_commit_link project, sha
- gitlab_url = project.gitlab_url.dup
- gitlab_url << "/commit/#{sha}"
- link_to Ci::Commit.truncate_sha(sha), gitlab_url, no_turbolink
- end
-
def yaml_web_editor_link(project)
commits = project.commits
- if commits.any? && commits.last.push_data[:ci_yaml_file]
- "#{@project.gitlab_url}/edit/master/.gitlab-ci.yml"
+ if commits.any? && commits.last.ci_yaml_file
+ "#{project.gitlab_url}/edit/master/.gitlab-ci.yml"
else
- "#{@project.gitlab_url}/new/master"
+ "#{project.gitlab_url}/new/master"
end
end
end
diff --git a/app/helpers/ci/icons_helper.rb b/app/helpers/ci/icons_helper.rb
deleted file mode 100644
index be40f79e880..00000000000
--- a/app/helpers/ci/icons_helper.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module Ci
- module IconsHelper
- def boolean_to_icon(value)
- if value.to_s == "true"
- content_tag :i, nil, class: 'fa fa-circle cgreen'
- else
- content_tag :i, nil, class: 'fa fa-power-off clgray'
- end
- end
- end
-end
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
deleted file mode 100644
index 03c9914641e..00000000000
--- a/app/helpers/ci/runners_helper.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module Ci
- module RunnersHelper
- def runner_status_icon(runner)
- unless runner.contacted_at
- return content_tag :i, nil,
- class: "fa fa-warning-sign",
- title: "New runner. Has not connected yet"
- end
-
- status =
- if runner.active?
- runner.contacted_at > 3.hour.ago ? :online : :offline
- else
- :paused
- end
-
- content_tag :i, nil,
- class: "fa fa-circle runner-status-#{status}",
- title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago"
- end
- end
-end
diff --git a/app/helpers/ci/triggers_helper.rb b/app/helpers/ci/triggers_helper.rb
deleted file mode 100644
index 0d2438928ce..00000000000
--- a/app/helpers/ci/triggers_helper.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Ci
- module TriggersHelper
- def ci_build_trigger_url(project_id, ref_name)
- "#{Settings.gitlab_ci.url}/ci/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger"
- end
- end
-end
diff --git a/app/helpers/ci/user_helper.rb b/app/helpers/ci/user_helper.rb
deleted file mode 100644
index c332d6ed9cf..00000000000
--- a/app/helpers/ci/user_helper.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Ci
- module UserHelper
- def user_avatar_url(user = nil, size = nil, default = 'identicon')
- size = 40 if size.nil? || size <= 0
-
- if user.blank? || user.avatar_url.blank?
- 'ci/no_avatar.png'
- elsif /^(http(s?):\/\/(www|secure)\.gravatar\.com\/avatar\/(\w*))/ =~ user.avatar_url
- Regexp.last_match[0] + "?s=#{size}&d=#{default}"
- else
- user.avatar_url
- end
- end
- end
-end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 18c30ddb281..0ecf77bb45e 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -1,11 +1,35 @@
module CiStatusHelper
def ci_status_path(ci_commit)
- ci_project_ref_commits_path(ci_commit.project, ci_commit.ref, ci_commit)
+ project = ci_commit.gl_project
+ builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha)
end
def ci_status_icon(ci_commit)
+ ci_icon_for_status(ci_commit.status)
+ end
+
+ def ci_status_color(ci_commit)
+ case ci_commit.status
+ when 'success'
+ 'green'
+ when 'failed'
+ 'red'
+ when 'running', 'pending'
+ 'yellow'
+ else
+ 'gray'
+ end
+ end
+
+ def ci_status_with_icon(status)
+ content_tag :span, class: "ci-status ci-#{status}" do
+ ci_icon_for_status(status) + '&nbsp;'.html_safe + status
+ end
+ end
+
+ def ci_icon_for_status(status)
icon_name =
- case ci_commit.status
+ case status
when 'success'
'check'
when 'failed'
@@ -19,16 +43,12 @@ module CiStatusHelper
icon(icon_name)
end
- def ci_status_color(ci_commit)
- case ci_commit.status
- when 'success'
- 'green'
- when 'failed'
- 'red'
- when 'running', 'pending'
- 'yellow'
- else
- 'gray'
+ def render_ci_status(ci_commit)
+ link_to ci_status_path(ci_commit),
+ class: "c#{ci_status_color(ci_commit)}",
+ title: "Build status: #{ci_commit.status}",
+ data: { toggle: 'tooltip', placement: 'left' } do
+ ci_status_icon(ci_commit)
end
end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 9df20c9fce5..590d20ac7b3 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -109,7 +109,7 @@ module CommitsHelper
)
elsif @path.present?
return link_to(
- "Browse Dir »",
+ "Browse Directory »",
namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "pull-right"
@@ -117,7 +117,7 @@ module CommitsHelper
end
end
link_to(
- "Browse Code »",
+ "Browse Files »",
namespace_project_tree_path(project.namespace, project, commit),
class: "pull-right"
)
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 6ffa1a7121d..24134310fc5 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -1,4 +1,8 @@
module DiffHelper
+ def diff_view
+ params[:view] == 'parallel' ? 'parallel' : 'inline'
+ end
+
def allowed_diff_size
if diff_hard_limit_enabled?
Commit::DIFF_HARD_LIMIT_FILES
@@ -132,33 +136,19 @@ module DiffHelper
end
def inline_diff_btn
- params_copy = params.dup
- params_copy[:view] = 'inline'
- # Always use HTML to handle case where JSON diff rendered this button
- params_copy.delete(:format)
-
- link_to url_for(params_copy), id: "inline-diff-btn", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do
- 'Inline'
- end
+ diff_btn('Inline', 'inline', diff_view == 'inline')
end
def parallel_diff_btn
- params_copy = params.dup
- params_copy[:view] = 'parallel'
- # Always use HTML to handle case where JSON diff rendered this button
- params_copy.delete(:format)
-
- link_to url_for(params_copy), id: "parallel-diff-btn", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do
- 'Side-by-side'
- end
+ diff_btn('Side-by-side', 'parallel', diff_view == 'parallel')
end
def submodule_link(blob, ref, repository = @repository)
tree, commit = submodule_links(blob, ref, repository)
commit_id = if commit.nil?
- blob.id[0..10]
+ Commit.truncate_sha(blob.id)
else
- link_to "#{blob.id[0..10]}", commit
+ link_to Commit.truncate_sha(blob.id), commit
end
[
@@ -167,4 +157,38 @@ module DiffHelper
content_tag(:span, commit_id, class: 'monospace'),
].join(' ').html_safe
end
+
+ def commit_for_diff(diff)
+ if diff.deleted_file
+ first_commit = @first_commit || @commit
+ first_commit.parent || @first_commit
+ else
+ @commit
+ end
+ end
+
+ def diff_file_html_data(project, diff_commit, diff_file)
+ {
+ blob_diff_path: namespace_project_blob_diff_path(project.namespace, project,
+ tree_join(diff_commit.id, diff_file.file_path))
+ }
+ end
+
+ def editable_diff?(diff)
+ !diff.deleted_file && @merge_request && @merge_request.source_project
+ end
+
+ private
+
+ def diff_btn(title, name, selected)
+ params_copy = params.dup
+ params_copy[:view] = name
+
+ # Always use HTML to handle case where JSON diff rendered this button
+ params_copy.delete(:format)
+
+ link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn') do
+ title
+ end
+ end
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 45788ba95ac..41b5bd7be90 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -28,6 +28,8 @@ module EmailsHelper
return "View #{action.humanize.singularize}"
end
end
+
+ nil
end
def color_email_diff(diffcontent)
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 6f69c2a9f32..dde83ff36b5 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -108,19 +108,23 @@ module EventsHelper
end
end
elsif event.push?
- if event.push_with_commits? && event.md_ref?
- if event.commits_count > 1
- namespace_project_compare_url(event.project.namespace, event.project,
- from: event.commit_from, to:
- event.commit_to)
- else
- namespace_project_commit_url(event.project.namespace, event.project,
- id: event.commit_to)
- end
+ push_event_feed_url(event)
+ end
+ end
+
+ def push_event_feed_url(event)
+ if event.push_with_commits? && event.md_ref?
+ if event.commits_count > 1
+ namespace_project_compare_url(event.project.namespace, event.project,
+ from: event.commit_from, to:
+ event.commit_to)
else
- namespace_project_commits_url(event.project.namespace, event.project,
- event.ref_name)
+ namespace_project_commit_url(event.project.namespace, event.project,
+ id: event.commit_to)
end
+ else
+ namespace_project_commits_url(event.project.namespace, event.project,
+ event.ref_name)
end
end
@@ -198,7 +202,7 @@ module EventsHelper
xml.link href: event_link
xml.title truncate(event_title, length: 80)
xml.updated event.created_at.xmlschema
- xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(event.author_email)
+ xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
xml.author do |author|
xml.name event.author_name
xml.email event.author_email
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 153a44870f6..98c6d9d5d2e 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -19,9 +19,10 @@ module GitlabMarkdownHelper
escape_once(body)
end
- gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: current_user)
+ user = current_user if defined?(current_user)
+ gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: user)
- fragment = Nokogiri::XML::DocumentFragment.parse(gfm_body)
+ fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a'
# Fragment has only one node, and it's a link generated by `gfm`.
# Replace it with our requested link.
@@ -45,29 +46,13 @@ module GitlabMarkdownHelper
end
def markdown(text, context = {})
- context.reverse_merge!(
- current_user: current_user,
- path: @path,
- project: @project,
- project_wiki: @project_wiki,
- ref: @ref
- )
-
- Gitlab::Markdown.render(text, context)
+ process_markdown(text, context)
end
# TODO (rspeicher): Remove all usages of this helper and just call `markdown`
# with a custom pipeline depending on the content being rendered
def gfm(text, options = {})
- options.reverse_merge!(
- current_user: current_user,
- path: @path,
- project: @project,
- project_wiki: @project_wiki,
- ref: @ref
- )
-
- Gitlab::Markdown.gfm(text, options)
+ process_markdown(text, options, :gfm)
end
def asciidoc(text)
@@ -193,4 +178,26 @@ module GitlabMarkdownHelper
''
end
end
+
+ def process_markdown(text, options, method = :markdown)
+ return "" unless text.present?
+
+ options.reverse_merge!(
+ path: @path,
+ pipeline: :default,
+ project: @project,
+ project_wiki: @project_wiki,
+ ref: @ref
+ )
+
+ user = current_user if defined?(current_user)
+
+ html = if method == :gfm
+ Gitlab::Markdown.gfm(text, options)
+ else
+ Gitlab::Markdown.render(text, options)
+ end
+
+ Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user)
+ end
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index e0816f4e714..f3fddef01cb 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -6,7 +6,7 @@
#
# For example instead of this:
#
-# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.projects, merge_request)
+# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
#
# We can simply use shortcut:
#
@@ -25,6 +25,10 @@ module GitlabRoutingHelper
namespace_project_commits_path(project.namespace, project, @ref || project.repository.root_ref)
end
+ def project_builds_path(project, *args)
+ namespace_project_builds_path(project.namespace, project, *args)
+ end
+
def activity_project_path(project, *args)
activity_namespace_project_path(project.namespace, project, *args)
end
@@ -33,6 +37,14 @@ module GitlabRoutingHelper
edit_namespace_project_path(project.namespace, project, *args)
end
+ def runners_path(project, *args)
+ namespace_project_runners_path(project.namespace, project, *args)
+ end
+
+ def runner_path(runner, *args)
+ namespace_project_runner_path(@project.namespace, @project, runner, *args)
+ end
+
def issue_path(entity, *args)
namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 1cf5b96481a..5724d3aabec 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -27,16 +27,20 @@ module IconsHelper
end
end
- def public_icon
- icon('globe fw')
- end
-
- def internal_icon
- icon('shield fw')
- end
+ def visibility_level_icon(level, fw: true)
+ name =
+ case level
+ when Gitlab::VisibilityLevel::PRIVATE
+ 'lock'
+ when Gitlab::VisibilityLevel::INTERNAL
+ 'shield'
+ else # Gitlab::VisibilityLevel::PUBLIC
+ 'globe'
+ end
+
+ name << " fw" if fw
- def private_icon
- icon('lock fw')
+ icon(name)
end
def file_type_icon_class(type, mode, name)
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 6ddb37cd0dc..e66b9c628c7 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -44,14 +44,17 @@ module IssuesHelper
end
def bulk_update_milestone_options
- options_for_select([['None (backlog)', -1]]) +
- options_from_collection_for_select(project_active_milestones, 'id',
- 'title', params[:milestone_id])
+ milestones = project_active_milestones.to_a
+ milestones.unshift(Milestone::None)
+
+ options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id])
end
def milestone_options(object)
- options_from_collection_for_select(object.project.milestones.active,
- 'id', 'title', object.milestone_id)
+ milestones = object.project.milestones.active.to_a
+ milestones.unshift(Milestone::None)
+
+ options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id)
end
def issue_box_class(item)
@@ -74,7 +77,7 @@ module IssuesHelper
issue.project, issue)
xml.title truncate(issue.title, length: 80)
xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ")
- xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(issue.author_email)
+ xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email))
xml.author do |author|
xml.name issue.author_name
xml.email issue.author_email
@@ -83,6 +86,41 @@ module IssuesHelper
end
end
+ def merge_requests_sentence(merge_requests)
+ # Sorting based on the `!123` or `group/project!123` reference will sort
+ # local merge requests first.
+ merge_requests.map do |merge_request|
+ merge_request.to_reference(@project)
+ end.sort.to_sentence(last_word_connector: ', or ')
+ end
+
+ def url_to_emoji(name)
+ emoji_path = ::AwardEmoji.path_to_emoji_image(name)
+ url_to_image(emoji_path)
+ rescue StandardError
+ ""
+ end
+
+ def emoji_author_list(notes, current_user)
+ list = notes.map do |note|
+ note.author == current_user ? "me" : note.author.name
+ end
+
+ list.join(", ")
+ end
+
+ def emoji_list
+ ::AwardEmoji::EMOJI_LIST
+ end
+
+ def note_active_class(notes, current_user)
+ if current_user && notes.pluck(:author_id).include?(current_user.id)
+ "active"
+ else
+ ""
+ end
+ end
+
# Required for Gitlab::Markdown::IssueReferenceFilter
module_function :url_for_issue
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 8036303851b..795fb439f25 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -92,8 +92,19 @@ module LabelsHelper
end
end
- def project_labels_options(project)
- options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name])
+ def projects_labels_options
+ labels =
+ if @project
+ @project.labels
+ else
+ Label.where(project_id: @projects)
+ end
+
+ grouped_labels = GlobalLabel.build_collection(labels)
+ grouped_labels.unshift(Label::None)
+ grouped_labels.unshift(Label::Any)
+
+ options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
end
# Required for Gitlab::Markdown::LabelReferenceFilter
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index f8169b4f288..6c32647594d 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -8,14 +8,6 @@ module MergeRequestsHelper
)
end
- def new_mr_path_for_fork_from_push_event(event)
- new_namespace_project_merge_request_path(
- event.project.namespace,
- event.project,
- new_mr_from_push_event(event, event.project.forked_from_project)
- )
- end
-
def new_mr_from_push_event(event, target_project)
{
merge_request: {
@@ -47,7 +39,11 @@ module MergeRequestsHelper
end
def issues_sentence(issues)
- issues.map { |i| "##{i.iid}" }.to_sentence
+ # Sorting based on the `#123` or `group/project#123` reference will sort
+ # local issues first.
+ issues.map do |issue|
+ issue.to_reference(@project)
+ end.sort.to_sentence
end
def mr_change_branches_path(merge_request)
@@ -57,18 +53,34 @@ module MergeRequestsHelper
source_project_id: @merge_request.source_project_id,
target_project_id: @merge_request.target_project_id,
source_branch: @merge_request.source_branch,
- target_branch: nil
- }
+ target_branch: @merge_request.target_branch,
+ },
+ change_branches: true
)
end
def source_branch_with_namespace(merge_request)
+ branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
+
if merge_request.for_fork?
namespace = link_to(merge_request.source_project_namespace,
project_path(merge_request.source_project))
- namespace + ":#{merge_request.source_branch}"
+ namespace + ":" + branch
+ else
+ branch
+ end
+ end
+
+ def format_mr_branch_names(merge_request)
+ source_path = merge_request.source_project_path
+ target_path = merge_request.target_project_path
+ source_branch = merge_request.source_branch
+ target_branch = merge_request.target_branch
+
+ if source_path == target_path
+ [source_branch, target_branch]
else
- merge_request.source_branch
+ ["#{source_path}:#{source_branch}", "#{target_path}:#{target_branch}"]
end
end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 132a893e532..a42cbcff182 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -28,9 +28,12 @@ module MilestonesHelper
Milestone.where(project_id: @projects)
end.active
- grouped_milestones = Milestones::GroupService.new(milestones).execute
+ epoch = DateTime.parse('1970-01-01')
+ grouped_milestones = GlobalMilestone.build_collection(milestones)
+ grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None)
+ grouped_milestones.unshift(Milestone::Any)
- options_from_collection_for_select(grouped_milestones, 'title', 'title', params[:milestone_title])
+ options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
end
end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index b3132a1f3ba..faba418c4db 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -1,10 +1,10 @@
module NamespacesHelper
- def namespaces_options(selected = :current_user, scope = :default)
+ def namespaces_options(selected = :current_user, display_path: false)
groups = current_user.owned_groups + current_user.masters_groups
users = [current_user.namespace]
- group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ]
- users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [u.human_name, u.id]} ]
+ group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [display_path ? g.path : g.human_name, g.id]} ]
+ users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [display_path ? u.path : u.human_name, u.id]} ]
options = []
options << group_opts
@@ -17,15 +17,6 @@ module NamespacesHelper
grouped_options_for_select(options, selected)
end
- def namespace_select_tag(id, opts = {})
- css_class = "ajax-namespace-select "
- css_class << "multiselect " if opts[:multiple]
- css_class << (opts[:class] || '')
- value = opts[:selected] || ''
-
- hidden_field_tag(id, value, class: css_class)
- end
-
def namespace_icon(namespace, size = 40)
if namespace.kind_of?(Group)
group_icon(namespace)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 9b1dd8b8e54..e6fb8670e57 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -5,6 +5,14 @@ module NavHelper
def nav_sidebar_class
if nav_menu_collapsed?
+ "sidebar-collapsed"
+ else
+ "sidebar-expanded"
+ end
+ end
+
+ def page_sidebar_class
+ if nav_menu_collapsed?
"page-sidebar-collapsed"
else
"page-sidebar-expanded"
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index cf11f8e5320..499c655d2bf 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -16,40 +16,28 @@ module NotificationsHelper
def notification_list_item(notification_level, user_membership)
case notification_level
when Notification::N_DISABLED
- content_tag(:li, class: active_level_for(user_membership, Notification::N_DISABLED)) do
- link_to '#', class: 'update-notification', data: { notification_level: Notification::N_DISABLED } do
- icon('microphone-slash fw', text: 'Disabled')
- end
- end
+ update_notification_link(Notification::N_DISABLED, user_membership, 'Disabled', 'microphone-slash')
when Notification::N_PARTICIPATING
- content_tag(:li, class: active_level_for(user_membership, Notification::N_PARTICIPATING)) do
- link_to '#', class: 'update-notification', data: { notification_level: Notification::N_PARTICIPATING } do
- icon('volume-up fw', text: 'Participate')
- end
- end
+ update_notification_link(Notification::N_PARTICIPATING, user_membership, 'Participate', 'volume-up')
when Notification::N_WATCH
- content_tag(:li, class: active_level_for(user_membership, Notification::N_WATCH)) do
- link_to '#', class: 'update-notification', data: { notification_level: Notification::N_WATCH } do
- icon('eye fw', text: 'Watch')
- end
- end
+ update_notification_link(Notification::N_WATCH, user_membership, 'Watch', 'eye')
when Notification::N_MENTION
- content_tag(:li, class: active_level_for(user_membership, Notification::N_MENTION)) do
- link_to '#', class: 'update-notification', data: { notification_level: Notification::N_MENTION } do
- icon('at fw', text: 'On mention')
- end
- end
+ update_notification_link(Notification::N_MENTION, user_membership, 'On mention', 'at')
when Notification::N_GLOBAL
- content_tag(:li, class: active_level_for(user_membership, Notification::N_GLOBAL)) do
- link_to '#', class: 'update-notification', data: { notification_level: Notification::N_GLOBAL } do
- icon('globe fw', text: 'Global')
- end
- end
+ update_notification_link(Notification::N_GLOBAL, user_membership, 'Global', 'globe')
else
# do nothing
end
end
+ def update_notification_link(notification_level, user_membership, title, icon)
+ content_tag(:li, class: active_level_for(user_membership, notification_level)) do
+ link_to '#', class: 'update-notification', data: { notification_level: notification_level } do
+ icon("#{icon} fw", text: title)
+ end
+ end
+ end
+
def notification_label(user_membership)
Notification.new(user_membership).to_s
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index df37be51ce9..775cf5a3dd4 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -26,7 +26,7 @@ module PageLayoutHelper
def fluid_layout(enabled = false)
if @fluid_layout.nil?
- @fluid_layout = enabled
+ @fluid_layout = (current_user && current_user.layout == "fluid") || enabled
else
@fluid_layout
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 7f1b6a69926..c73cb3028ee 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -1,9 +1,18 @@
# Helper methods for per-User preferences
module PreferencesHelper
+ def layout_choices
+ [
+ ['Fixed', :fixed],
+ ['Fluid', :fluid]
+ ]
+ end
+
# Maps `dashboard` values to more user-friendly option text
DASHBOARD_CHOICES = {
projects: 'Your Projects (default)',
- stars: 'Starred Projects'
+ stars: 'Starred Projects',
+ project_activity: "Your Projects' Activity",
+ starred_project_activity: "Starred Projects' Activity"
}.with_indifferent_access.freeze
# Returns an Array usable by a select field for more user-friendly option text
@@ -25,7 +34,8 @@ module PreferencesHelper
def project_view_choices
[
['Readme (default)', :readme],
- ['Activity view', :activity]
+ ['Activity view', :activity],
+ ['Files view', :files]
]
end
@@ -37,8 +47,7 @@ module PreferencesHelper
Gitlab::ColorSchemes.for_user(current_user).css_class
end
- def prefer_readme?
- !current_user ||
- current_user.project_view == 'readme'
+ def default_project_view
+ current_user ? current_user.project_view : 'readme'
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 7b4747ce3d7..48729e5260e 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -21,7 +21,7 @@ module ProjectsHelper
end
def link_to_member(project, author, opts = {})
- default_opts = { avatar: true, name: true, size: 16, author_class: 'author' }
+ default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
opts = default_opts.merge(opts)
return "(deleted)" unless author
@@ -29,7 +29,7 @@ module ProjectsHelper
author_html = ""
# Build avatar image tag
- author_html << image_tag(avatar_icon(author.try(:email), opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
+ 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]
@@ -39,7 +39,8 @@ module ProjectsHelper
if opts[:name]
link_to(author_html, user_path(author), class: "author_link").html_safe
else
- link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => sanitize(author.name) } ).html_safe
+ title = opts[:title].sub(":name", sanitize(author.name))
+ link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => title, container: 'body' } ).html_safe
end
end
@@ -70,6 +71,10 @@ module ProjectsHelper
"You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
end
+ def remove_fork_project_message(project)
+ "You are going to remove the fork relationship to source project #{@project.forked_from_project.name_with_namespace}. Are you ABSOLUTELY sure?"
+ end
+
def project_nav_tabs
@nav_tabs ||= get_project_nav_tabs(@project, current_user)
end
@@ -113,6 +118,10 @@ module ProjectsHelper
nav_tabs << :merge_requests
end
+ if project.builds_enabled? && can?(current_user, :read_build, project)
+ nav_tabs << :builds
+ end
+
if can?(current_user, :admin_project, project)
nav_tabs << :settings
end
@@ -165,8 +174,7 @@ module ProjectsHelper
'unknown'
end
- def default_url_to_repo(project = nil)
- project = project || @project
+ def default_url_to_repo(project = @project)
current_user ? project.url_to_repo : project.http_url_to_repo
end
@@ -245,14 +253,6 @@ module ProjectsHelper
filename_path(project, :version)
end
- def hidden_pass_url(original_url)
- result = URI(original_url)
- result.password = '*****' unless result.password.nil?
- result
- rescue
- original_url
- end
-
def project_wiki_path_with_version(proj, page, version, is_newest)
url_params = is_newest ? {} : { version_id: version }
namespace_project_wiki_path(proj.namespace, proj, page, url_params)
@@ -296,7 +296,7 @@ module ProjectsHelper
def readme_cache_key
sha = @project.commit.try(:sha) || 'nil'
- [@project.id, sha, "readme"].join('-')
+ [@project.path_with_namespace, sha, "readme"].join('-')
end
def round_commit_count(project)
diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb
new file mode 100644
index 00000000000..46eb82a354f
--- /dev/null
+++ b/app/helpers/runners_helper.rb
@@ -0,0 +1,29 @@
+module RunnersHelper
+ def runner_status_icon(runner)
+ status = runner.status
+ case status
+ when :not_connected
+ content_tag :i, nil,
+ class: "fa fa-warning",
+ title: "New runner. Has not connected yet"
+
+ when :online, :offline, :paused
+ content_tag :i, nil,
+ class: "fa fa-circle runner-status-#{status}",
+ title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago"
+ end
+ end
+
+ def runner_link(runner)
+ display_name = truncate(runner.display_name, length: 15)
+ id = "\##{runner.id}"
+
+ if current_user && current_user.admin
+ link_to ci_admin_runner_path(runner) do
+ display_name + id
+ end
+ else
+ display_name + id
+ end
+ end
+end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index c31a556ff7b..a6ee6880247 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -70,7 +70,7 @@ module SearchHelper
# Autocomplete results for the current user's groups
def groups_autocomplete(term, limit = 5)
- current_user.authorized_groups.search(term).limit(limit).map do |group|
+ GroupsFinder.new.execute(current_user).search(term).limit(limit).map do |group|
{
label: "group: #{search_result_sanitize(group.name)}",
url: group_path(group)
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 12fce8db701..7e175d0de8a 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -15,12 +15,14 @@ module SelectsHelper
html = {
class: css_class,
- 'data-placeholder' => placeholder,
- 'data-null-user' => null_user,
- 'data-any-user' => any_user,
- 'data-email-user' => email_user,
- 'data-first-user' => first_user,
- 'data-current-user' => current_user
+ data: {
+ placeholder: placeholder,
+ null_user: null_user,
+ any_user: any_user,
+ email_user: email_user,
+ first_user: first_user,
+ current_user: current_user
+ }
}
unless opts[:scope] == :all
@@ -35,8 +37,20 @@ module SelectsHelper
end
def groups_select_tag(id, opts = {})
- css_class = "ajax-groups-select "
- css_class << "multiselect " if opts[:multiple]
+ opts[:class] ||= ''
+ opts[:class] << ' ajax-groups-select'
+ select2_tag(id, opts)
+ end
+
+ def namespace_select_tag(id, opts = {})
+ opts[:class] ||= ''
+ opts[:class] << ' ajax-namespace-select'
+ select2_tag(id, opts)
+ end
+
+ def select2_tag(id, opts = {})
+ css_class = ''
+ css_class << 'multiselect ' if opts[:multiple]
css_class << (opts[:class] || '')
value = opts[:selected] || ''
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 0e7d8065ac7..04e53fe7c61 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -110,22 +110,4 @@ module TabHelper
'active'
end
end
-
- # Use nav_tab for save controller/action but different params
- def nav_tab(key, value, &block)
- o = {}
- o[:class] = ""
-
- if value.nil?
- o[:class] << " active" if params[key].blank?
- else
- o[:class] << " active" if params[key] == value
- end
-
- if block_given?
- content_tag(:li, capture(&block), o)
- else
- content_tag(:li, nil, o)
- end
- end
end
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
new file mode 100644
index 00000000000..8142f733e76
--- /dev/null
+++ b/app/helpers/time_helper.rb
@@ -0,0 +1,27 @@
+module TimeHelper
+ def duration_in_words(finished_at, started_at)
+ if finished_at && started_at
+ interval_in_seconds = finished_at.to_i - started_at.to_i
+ elsif started_at
+ interval_in_seconds = Time.now.to_i - started_at.to_i
+ end
+
+ time_interval_in_words(interval_in_seconds)
+ end
+
+ def time_interval_in_words(interval_in_seconds)
+ minutes = interval_in_seconds / 60
+ seconds = interval_in_seconds - minutes * 60
+
+ if minutes >= 1
+ "#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}"
+ else
+ "#{pluralize(seconds, "second")}"
+ end
+ end
+
+
+ def date_from_to(from, to)
+ "#{from.to_s(:short)} - #{to.to_s(:short)}"
+ end
+end
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
new file mode 100644
index 00000000000..2a3a7e80fca
--- /dev/null
+++ b/app/helpers/triggers_helper.rb
@@ -0,0 +1,5 @@
+module TriggersHelper
+ def ci_build_trigger_url(project_id, ref_name)
+ "#{Settings.gitlab_ci.url}/ci/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger"
+ end
+end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index b52cd23aba2..72c65030f94 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -25,48 +25,24 @@ module VisibilityLevelHelper
end
def project_visibility_level_description(level)
- capture_haml do
- haml_tag :span do
- case level
- when Gitlab::VisibilityLevel::PRIVATE
- haml_concat "Project access must be granted explicitly for each user."
- when Gitlab::VisibilityLevel::INTERNAL
- haml_concat "The project can be cloned by"
- haml_concat "any logged in user."
- when Gitlab::VisibilityLevel::PUBLIC
- haml_concat "The project can be cloned"
- haml_concat "without any"
- haml_concat "authentication."
- end
- end
+ case level
+ when Gitlab::VisibilityLevel::PRIVATE
+ "Project access must be granted explicitly for each user."
+ when Gitlab::VisibilityLevel::INTERNAL
+ "The project can be cloned by any logged in user."
+ when Gitlab::VisibilityLevel::PUBLIC
+ "The project can be cloned without any authentication."
end
end
def snippet_visibility_level_description(level)
- capture_haml do
- haml_tag :span do
- case level
- when Gitlab::VisibilityLevel::PRIVATE
- haml_concat "The snippet is visible only for me."
- when Gitlab::VisibilityLevel::INTERNAL
- haml_concat "The snippet is visible for any logged in user."
- when Gitlab::VisibilityLevel::PUBLIC
- haml_concat "The snippet can be accessed"
- haml_concat "without any"
- haml_concat "authentication."
- end
- end
- end
- end
-
- def visibility_level_icon(level)
case level
when Gitlab::VisibilityLevel::PRIVATE
- private_icon
+ "The snippet is visible only for me."
when Gitlab::VisibilityLevel::INTERNAL
- internal_icon
+ "The snippet is visible for any logged in user."
when Gitlab::VisibilityLevel::PUBLIC
- public_icon
+ "The snippet can be accessed without any authentication."
end
end
diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb
new file mode 100644
index 00000000000..f0c41f69a5c
--- /dev/null
+++ b/app/mailers/abuse_report_mailer.rb
@@ -0,0 +1,12 @@
+class AbuseReportMailer < BaseMailer
+ include Gitlab::CurrentSettings
+
+ def notify(abuse_report_id)
+ @abuse_report = AbuseReport.find(abuse_report_id)
+
+ mail(
+ to: current_application_settings.admin_notification_email,
+ subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse"
+ )
+ end
+end
diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index aedb0889185..8b83bbd93b7 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -8,10 +8,6 @@ class BaseMailer < ActionMailer::Base
default from: Proc.new { default_sender_address.format }
default reply_to: Proc.new { default_reply_to_address.format }
- def self.delay
- delay_for(2.seconds)
- end
-
def can?
Ability.abilities.allowed?(current_user, action, subject)
end
diff --git a/app/mailers/ci/notify.rb b/app/mailers/ci/notify.rb
index 4462da0d7d2..404842cf213 100644
--- a/app/mailers/ci/notify.rb
+++ b/app/mailers/ci/notify.rb
@@ -2,7 +2,6 @@ module Ci
class Notify < ActionMailer::Base
include Ci::Emails::Builds
- add_template_helper Ci::ApplicationHelper
add_template_helper Ci::GitlabHelper
default_url_options[:host] = Gitlab.config.gitlab.host
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 2c035fbb70b..abdeefed5ef 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -1,53 +1,49 @@
module Emails
module Issues
def new_issue_email(recipient_id, issue_id)
- @issue = Issue.find(issue_id)
- @project = @issue.project
- @target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
- mail_new_thread(@issue,
- from: sender(@issue.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@issue.title} (##{@issue.iid})"))
-
- SentNotification.record(@issue, recipient_id, reply_key)
+ issue_mail_with_notification(issue_id, recipient_id) do
+ mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
+ end
end
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
- @issue = Issue.find(issue_id)
- @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- @project = @issue.project
- @target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
- mail_answer_thread(@issue,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@issue.title} (##{@issue.iid})"))
-
- SentNotification.record(@issue, recipient_id, reply_key)
+ issue_mail_with_notification(issue_id, recipient_id) do
+ @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+ end
end
def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
- @issue = Issue.find issue_id
- @project = @issue.project
- @updated_by = User.find updated_by_user_id
- @target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
- mail_answer_thread(@issue,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@issue.title} (##{@issue.iid})"))
-
- SentNotification.record(@issue, recipient_id, reply_key)
+ issue_mail_with_notification(issue_id, recipient_id) do
+ @updated_by = User.find updated_by_user_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+ end
end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id)
- @issue = Issue.find issue_id
- @issue_status = status
+ issue_mail_with_notification(issue_id, recipient_id) do
+ @issue_status = status
+ @updated_by = User.find updated_by_user_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+ end
+ end
+
+ private
+
+ def issue_thread_options(sender_id, recipient_id)
+ {
+ from: sender(sender_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@issue.title} (##{@issue.iid})")
+ }
+ end
+
+ def issue_mail_with_notification(issue_id, recipient_id)
+ @issue = Issue.find(issue_id)
@project = @issue.project
- @updated_by = User.find updated_by_user_id
@target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
- mail_answer_thread(@issue,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@issue.title} (##{@issue.iid})"))
+
+ yield
SentNotification.record(@issue, recipient_id, reply_key)
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 87ba94a583d..65f37e92677 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -1,49 +1,54 @@
module Emails
module Notes
def note_commit_email(recipient_id, note_id)
- @note = Note.find(note_id)
- @commit = @note.noteable
- @project = @note.project
- @target_url = namespace_project_commit_url(@project.namespace, @project,
- @commit, anchor:
- "note_#{@note.id}")
- mail_answer_thread(@commit,
- from: sender(@note.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@commit.title} (#{@commit.short_id})"))
-
- SentNotification.record_note(@note, recipient_id, reply_key)
+ 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
end
def note_issue_email(recipient_id, note_id)
- @note = Note.find(note_id)
- @issue = @note.noteable
- @project = @note.project
- @target_url = namespace_project_issue_url(@project.namespace, @project,
- @issue, anchor:
- "note_#{@note.id}")
- mail_answer_thread(@issue,
- from: sender(@note.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@issue.title} (##{@issue.iid})"))
-
- SentNotification.record_note(@note, recipient_id, reply_key)
+ note_mail_with_notification(note_id, recipient_id) do
+ @issue = @note.noteable
+ @target_url = namespace_project_issue_url(*note_target_url_options)
+ mail_answer_thread(@issue, note_thread_options(recipient_id))
+ end
end
def note_merge_request_email(recipient_id, note_id)
+ note_mail_with_notification(note_id, recipient_id) do
+ @merge_request = @note.noteable
+ @target_url = namespace_project_merge_request_url(*note_target_url_options)
+ mail_answer_thread(@merge_request, note_thread_options(recipient_id))
+ end
+ end
+
+ private
+
+ def note_target_url_options
+ [@project.namespace, @project, @note.noteable, anchor: "note_#{@note.id}"]
+ end
+
+ def note_thread_options(recipient_id)
+ {
+ from: sender(@note.author_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@note.noteable.title} (##{@note.noteable.iid})")
+ }
+ end
+
+ def note_mail_with_notification(note_id, recipient_id)
@note = Note.find(note_id)
- @merge_request = @note.noteable
@project = @note.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request, anchor:
- "note_#{@note.id}")
- mail_answer_thread(@merge_request,
- from: sender(@note.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
-
- SentNotification.record_note(@note, recipient_id, reply_key)
+
+ yield
+
+ SentNotification.record(@note, recipient_id, reply_key)
end
end
end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 4a6e18e6a74..caba63006da 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -50,10 +50,11 @@ module Emails
subject: subject("Invitation declined"))
end
- def project_was_moved_email(project_id, user_id)
+ def project_was_moved_email(project_id, user_id, old_path_with_namespace)
@current_user = @user = User.find user_id
@project = Project.find project_id
@target_url = namespace_project_url(@project.namespace, @project)
+ @old_path_with_namespace = old_path_with_namespace
mail(to: @user.notification_email,
subject: subject("Project was moved"))
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index db2f9654e14..50a409c3754 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -140,7 +140,7 @@ class Notify < BaseMailer
# * have a 'In-Reply-To' or 'References' header that references the original 'Message-ID'
#
def mail_answer_thread(model, headers = {})
- headers['Message-ID'] = SecureRandom.hex
+ headers['Message-ID'] = "<#{SecureRandom.hex}@#{Gitlab.config.gitlab.host}>"
headers['In-Reply-To'] = message_id(model)
headers['References'] = message_id(model)
diff --git a/app/models/ability.rb b/app/models/ability.rb
index a020b24a550..07f3a56ec7a 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -1,8 +1,8 @@
class Ability
class << self
def allowed(user, subject)
- return not_auth_abilities(user, subject) if user.nil?
- return [] unless user.kind_of?(User)
+ return anonymous_abilities(user, subject) if user.nil?
+ return [] unless user.is_a?(User)
return [] if user.blocked?
case subject.class.name
@@ -15,19 +15,30 @@ class Ability
when "Group" then group_abilities(user, subject)
when "Namespace" then namespace_abilities(user, subject)
when "GroupMember" then group_member_abilities(user, subject)
+ when "ProjectMember" then project_member_abilities(user, subject)
else []
end.concat(global_abilities(user))
end
- # List of possible abilities
- # for non-authenticated user
- def not_auth_abilities(user, subject)
- project = if subject.kind_of?(Project)
+ # List of possible abilities for anonymous user
+ def anonymous_abilities(user, subject)
+ case true
+ when subject.is_a?(PersonalSnippet)
+ anonymous_personal_snippet_abilities(subject)
+ when subject.is_a?(Project) || subject.respond_to?(:project)
+ anonymous_project_abilities(subject)
+ when subject.is_a?(Group) || subject.respond_to?(:group)
+ anonymous_group_abilities(subject)
+ else
+ []
+ end
+ end
+
+ def anonymous_project_abilities(subject)
+ project = if subject.is_a?(Project)
subject
- elsif subject.respond_to?(:project)
- subject.project
else
- nil
+ subject.project
end
if project && project.public?
@@ -41,24 +52,35 @@ class Ability
:read_project_member,
:read_merge_request,
:read_note,
+ :read_build,
:download_code
]
rules - project_disabled_features_rules(project)
else
- group = if subject.kind_of?(Group)
- subject
- elsif subject.respond_to?(:group)
- subject.group
- else
- nil
- end
+ []
+ end
+ end
- if group && group.public_profile?
- [:read_group]
- else
- []
- end
+ def anonymous_group_abilities(subject)
+ group = if subject.is_a?(Group)
+ subject
+ else
+ subject.group
+ end
+
+ if group && group.public_profile?
+ [:read_group]
+ else
+ []
+ end
+ end
+
+ def anonymous_personal_snippet_abilities(snippet)
+ if snippet.public?
+ [:read_personal_snippet]
+ else
+ []
end
end
@@ -127,6 +149,7 @@ class Ability
:read_project_member,
:read_merge_request,
:read_note,
+ :read_build,
:create_project,
:create_issue,
:create_note
@@ -135,6 +158,8 @@ class Ability
def project_report_rules
project_guest_rules + [
+ :create_commit_status,
+ :read_commit_statuses,
:download_code,
:fork_project,
:create_project_snippet,
@@ -150,6 +175,7 @@ class Ability
:create_merge_request,
:create_wiki,
:manage_builds,
+ :download_build_artifacts,
:push_code
]
end
@@ -185,7 +211,8 @@ class Ability
:change_visibility_level,
:rename_project,
:remove_project,
- :archive_project
+ :archive_project,
+ :remove_fork_project
]
end
@@ -225,18 +252,19 @@ class Ability
# Only group masters and group owners can create new projects in group
if group.has_master?(user) || group.has_owner?(user) || user.admin?
- rules.push(*[
+ rules += [
:create_projects,
- ])
+ :admin_milestones
+ ]
end
# Only group owner and administrators can admin group
if group.has_owner?(user) || user.admin?
- rules.push(*[
+ rules += [
:admin_group,
:admin_namespace,
:admin_group_member
- ])
+ ]
end
rules.flatten
@@ -247,16 +275,15 @@ class Ability
# Only namespace owner and administrators can admin it
if namespace.owner == user || user.admin?
- rules.push(*[
+ rules += [
:create_projects,
:admin_namespace
- ])
+ ]
end
rules.flatten
end
-
[:issue, :merge_request].each do |name|
define_method "#{name}_abilities" do |user, subject|
rules = []
@@ -273,7 +300,7 @@ class Ability
end
end
- [:note, :project_snippet, :personal_snippet].each do |name|
+ [:note, :project_snippet].each do |name|
define_method "#{name}_abilities" do |user, subject|
rules = []
@@ -293,19 +320,61 @@ class Ability
end
end
+ def personal_snippet_abilities(user, snippet)
+ rules = []
+
+ if snippet.author == user
+ rules += [
+ :read_personal_snippet,
+ :update_personal_snippet,
+ :admin_personal_snippet
+ ]
+ end
+
+ if snippet.public? || snippet.internal?
+ rules << :read_personal_snippet
+ end
+
+ rules
+ end
+
def group_member_abilities(user, subject)
rules = []
target_user = subject.user
group = subject.group
- can_manage = group_abilities(user, group).include?(:admin_group_member)
- if can_manage && (user != target_user)
- rules << :update_group_member
- rules << :destroy_group_member
+ unless group.last_owner?(target_user)
+ can_manage = group_abilities(user, group).include?(:admin_group_member)
+
+ if can_manage && user != target_user
+ rules << :update_group_member
+ rules << :destroy_group_member
+ end
+
+ if user == target_user
+ rules << :destroy_group_member
+ end
end
- if !group.last_owner?(user) && (can_manage || (user == target_user))
- rules << :destroy_group_member
+ rules
+ end
+
+ def project_member_abilities(user, subject)
+ rules = []
+ target_user = subject.user
+ project = subject.project
+
+ unless target_user == project.owner
+ can_manage = project_abilities(user, project).include?(:admin_project_member)
+
+ if can_manage && user != target_user
+ rules << :update_project_member
+ rules << :destroy_project_member
+ end
+
+ if user == target_user
+ rules << :destroy_project_member
+ end
end
rules
@@ -313,10 +382,10 @@ class Ability
def abilities
@abilities ||= begin
- abilities = Six.new
- abilities << self
- abilities
- end
+ abilities = Six.new
+ abilities << self
+ abilities
+ end
end
private
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 07c87a7fe87..89b3116b9f2 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -11,11 +11,11 @@
#
class AbuseReport < ActiveRecord::Base
- belongs_to :reporter, class_name: "User"
+ belongs_to :reporter, class_name: 'User'
belongs_to :user
validates :reporter, presence: true
validates :user, presence: true
validates :message, presence: true
- validates :user_id, uniqueness: { scope: :reporter_id }
+ validates :user_id, uniqueness: true
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 784f5c96a0a..1880ad9f33c 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -23,9 +23,15 @@
# after_sign_out_path :string(255)
# session_expire_delay :integer default(10080), not null
# import_sources :text
+# help_page_text :text
+# admin_notification_email :string(255)
+# shared_runners_enabled :boolean default(TRUE), not null
+# max_artifacts_size :integer default(100), not null
#
class ApplicationSetting < ActiveRecord::Base
+ CACHE_KEY = 'application_setting.last'
+
serialize :restricted_visibility_levels
serialize :import_sources
serialize :restricted_signup_domains, Array
@@ -37,12 +43,16 @@ class ApplicationSetting < ActiveRecord::Base
validates :home_page_url,
allow_blank: true,
- format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" },
+ url: true,
if: :home_page_url_column_exist
validates :after_sign_out_path,
allow_blank: true,
- format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }
+ url: true
+
+ validates :admin_notification_email,
+ allow_blank: true,
+ email: true
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
@@ -64,8 +74,18 @@ class ApplicationSetting < ActiveRecord::Base
end
end
+ after_commit do
+ Rails.cache.write(CACHE_KEY, self)
+ end
+
def self.current
- ApplicationSetting.last
+ Rails.cache.fetch(CACHE_KEY) do
+ ApplicationSetting.last
+ end
+ end
+
+ def self.expire
+ Rails.cache.delete(CACHE_KEY)
end
def self.create_from_defaults
@@ -84,7 +104,8 @@ class ApplicationSetting < ActiveRecord::Base
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
restricted_signup_domains: Settings.gitlab['restricted_signup_domains'],
import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'],
- ci_enabled: Settings.gitlab_ci['enabled']
+ shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
+ max_artifacts_size: Settings.artifacts['max_size'],
)
end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 05f5e979695..ad514706160 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -16,12 +16,12 @@
class BroadcastMessage < ActiveRecord::Base
include Sortable
- validates :message, presence: true
+ validates :message, presence: true
validates :starts_at, presence: true
- validates :ends_at, presence: true
+ validates :ends_at, presence: true
- validates :color, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true
- validates :font, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true
+ validates :color, allow_blank: true, color: true
+ validates :font, allow_blank: true, color: true
def self.current
where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last
diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb
index 0cf496f7d81..7f5df8ce6c4 100644
--- a/app/models/ci/application_setting.rb
+++ b/app/models/ci/application_setting.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: application_settings
+# Table name: ci_application_settings
#
# id :integer not null, primary key
# all_broken_builds :boolean
@@ -12,9 +12,20 @@
module Ci
class ApplicationSetting < ActiveRecord::Base
extend Ci::Model
-
+ CACHE_KEY = 'ci_application_setting.last'
+
+ after_commit do
+ Rails.cache.write(CACHE_KEY, self)
+ end
+
+ def self.expire
+ Rails.cache.delete(CACHE_KEY)
+ end
+
def self.current
- Ci::ApplicationSetting.last
+ Rails.cache.fetch(CACHE_KEY) do
+ Ci::ApplicationSetting.last
+ end
end
def self.create_from_defaults
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 8096d4fa5ae..52ce1b920fa 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: builds
+# Table name: ci_builds
#
# id :integer not null, primary key
# project_id :integer
@@ -11,41 +11,43 @@
# updated_at :datetime
# started_at :datetime
# runner_id :integer
-# commit_id :integer
# coverage :float
+# commit_id :integer
# commands :text
# job_id :integer
# name :string(255)
+# deploy :boolean default(FALSE)
# options :text
# allow_failure :boolean default(FALSE), not null
# stage :string(255)
-# deploy :boolean default(FALSE)
# trigger_request_id :integer
+# stage_idx :integer
+# tag :boolean
+# ref :string(255)
+# user_id :integer
+# type :string(255)
+# target_url :string(255)
+# description :string(255)
+# artifacts_file :text
#
module Ci
- class Build < ActiveRecord::Base
- extend Ci::Model
-
+ class Build < CommitStatus
LAZY_ATTRIBUTES = ['trace']
- belongs_to :commit, class_name: 'Ci::Commit'
- belongs_to :project, class_name: 'Ci::Project'
belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
serialize :options
- validates :commit, presence: true
- validates :status, presence: true
validates :coverage, numericality: true, allow_blank: true
+ validates_presence_of :ref
- scope :running, ->() { where(status: "running") }
- scope :pending, ->() { where(status: "pending") }
- scope :success, ->() { where(status: "success") }
- scope :failed, ->() { where(status: "failed") }
scope :unstarted, ->() { where(runner_id: nil) }
- scope :running_or_pending, ->() { where(status:[:running, :pending]) }
+ scope :ignore_failures, ->() { where(allow_failure: false) }
+ scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) }
+
+ mount_uploader :artifacts_file, ArtifactUploader
acts_as_taggable
@@ -69,21 +71,24 @@ module Ci
def create_from(build)
new_build = build.dup
- new_build.status = :pending
+ new_build.status = 'pending'
new_build.runner_id = nil
+ new_build.trigger_request_id = nil
new_build.save
end
def retry(build)
- new_build = Ci::Build.new(status: :pending)
+ new_build = Ci::Build.new(status: 'pending')
+ new_build.ref = build.ref
+ new_build.tag = build.tag
new_build.options = build.options
new_build.commands = build.commands
new_build.tag_list = build.tag_list
new_build.commit_id = build.commit_id
- new_build.project_id = build.project_id
new_build.name = build.name
new_build.allow_failure = build.allow_failure
new_build.stage = build.stage
+ new_build.stage_idx = build.stage_idx
new_build.trigger_request = build.trigger_request
new_build.save
new_build
@@ -91,80 +96,39 @@ module Ci
end
state_machine :status, initial: :pending do
- event :run do
- transition pending: :running
- end
-
- event :drop do
- transition running: :failed
- end
-
- event :success do
- transition running: :success
- end
-
- event :cancel do
- transition [:pending, :running] => :canceled
- end
-
- after_transition pending: :running do |build, transition|
- build.update_attributes started_at: Time.now
- end
-
after_transition any => [:success, :failed, :canceled] do |build, transition|
- build.update_attributes finished_at: Time.now
+ return unless build.gl_project
+
project = build.project
if project.web_hooks?
Ci::WebHookService.new.build_end(build)
end
- if build.commit.success?
- build.commit.create_next_builds(build.trigger_request)
- end
-
+ build.commit.create_next_builds(build)
project.execute_services(build)
if project.coverage_enabled?
build.update_coverage
end
end
-
- state :pending, value: 'pending'
- state :running, value: 'running'
- state :failed, value: 'failed'
- state :success, value: 'success'
- state :canceled, value: 'canceled'
- end
-
- delegate :sha, :short_sha, :before_sha, :ref,
- to: :commit, prefix: false
-
- def trace_html
- html = Ci::Ansi2html::convert(trace) if trace.present?
- html ||= ''
- end
-
- def trace
- if project && read_attribute(:trace).present?
- read_attribute(:trace).gsub(project.token, 'xxxxxx')
- end
end
- def started?
- !pending? && !canceled? && started_at
+ def ignored?
+ failed? && allow_failure?
end
- def active?
- running? || pending?
+ def retryable?
+ commands.present?
end
- def complete?
- canceled? || success? || failed?
+ def retried?
+ !self.commit.latest_builds_for_ref(self.ref).include?(self)
end
- def ignored?
- failed? && allow_failure?
+ def trace_html
+ html = Ci::Ansi2html::convert(trace) if trace.present?
+ html || ''
end
def timeout
@@ -172,15 +136,7 @@ module Ci
end
def variables
- yaml_variables + project_variables + trigger_variables
- end
-
- def duration
- if started_at && finished_at
- finished_at - started_at
- elsif started_at
- Time.now - started_at
- end
+ predefined_variables + yaml_variables + project_variables + trigger_variables
end
def project
@@ -188,13 +144,23 @@ module Ci
end
def project_id
- commit.project_id
+ commit.project.id
end
def project_name
project.name
end
+ def project_recipients
+ recipients = project.email_recipients.split(' ')
+
+ if project.email_add_pusher? && user.present? && user.notification_email.present?
+ recipients << user.notification_email
+ end
+
+ recipients.uniq
+ end
+
def repo_url
project.repo_url_with_auth
end
@@ -219,13 +185,13 @@ module Ci
if coverage.present?
coverage.to_f
end
- rescue => ex
+ rescue
# if bad regex or something goes wrong we dont want to interrupt transition
# so we just silentrly ignore error for now
end
end
- def trace
+ def raw_trace
if File.exist?(path_to_trace)
File.read(path_to_trace)
else
@@ -234,6 +200,15 @@ module Ci
end
end
+ def trace
+ trace = raw_trace
+ if project && trace.present?
+ trace.gsub(project.token, 'xxxxxx')
+ else
+ trace
+ end
+ end
+
def trace=(trace)
unless Dir.exists? dir_to_trace
FileUtils.mkdir_p dir_to_trace
@@ -254,6 +229,52 @@ module Ci
"#{dir_to_trace}/#{id}.log"
end
+ def token
+ project.token
+ end
+
+ def valid_token? token
+ project.valid_token? token
+ end
+
+ def target_url
+ Gitlab::Application.routes.url_helpers.
+ namespace_project_build_url(gl_project.namespace, gl_project, self)
+ end
+
+ def cancel_url
+ if active?
+ Gitlab::Application.routes.url_helpers.
+ cancel_namespace_project_build_path(gl_project.namespace, gl_project, self)
+ end
+ end
+
+ def retry_url
+ if retryable?
+ Gitlab::Application.routes.url_helpers.
+ retry_namespace_project_build_path(gl_project.namespace, gl_project, self)
+ end
+ end
+
+ def can_be_served?(runner)
+ (tag_list - runner.tag_list).empty?
+ end
+
+ def any_runners_online?
+ project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
+ end
+
+ def show_warning?
+ pending? && !any_runners_online?
+ end
+
+ def download_url
+ if artifacts_file.exists?
+ Gitlab::Application.routes.url_helpers.
+ download_namespace_project_build_path(gl_project.namespace, gl_project, self)
+ end
+ end
+
private
def yaml_variables
@@ -281,5 +302,14 @@ module Ci
[]
end
end
+
+ def predefined_variables
+ variables = []
+ variables << { key: :CI_BUILD_TAG, value: ref, public: true } if tag?
+ variables << { key: :CI_BUILD_NAME, value: name, public: true }
+ variables << { key: :CI_BUILD_STAGE, value: stage, public: true }
+ variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request
+ variables
+ end
end
end
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
index f102d0a7679..971e899de84 100644
--- a/app/models/ci/commit.rb
+++ b/app/models/ci/commit.rb
@@ -1,31 +1,33 @@
# == Schema Information
#
-# Table name: commits
+# Table name: ci_commits
#
-# id :integer not null, primary key
-# project_id :integer
-# ref :string(255)
-# sha :string(255)
-# before_sha :string(255)
-# push_data :text
-# created_at :datetime
-# updated_at :datetime
-# tag :boolean default(FALSE)
-# yaml_errors :text
-# committed_at :datetime
+# id :integer not null, primary key
+# project_id :integer
+# ref :string(255)
+# sha :string(255)
+# before_sha :string(255)
+# push_data :text
+# created_at :datetime
+# updated_at :datetime
+# tag :boolean default(FALSE)
+# yaml_errors :text
+# committed_at :datetime
+# gl_project_id :integer
#
module Ci
class Commit < ActiveRecord::Base
extend Ci::Model
-
- belongs_to :project, class_name: 'Ci::Project'
- has_many :builds, dependent: :destroy, class_name: 'Ci::Build'
+
+ belongs_to :gl_project, class_name: '::Project', foreign_key: :gl_project_id
+ has_many :statuses, dependent: :destroy, class_name: 'CommitStatus'
+ has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
- serialize :push_data
+ scope :ordered, -> { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }
- validates_presence_of :ref, :sha, :before_sha, :push_data
+ validates_presence_of :sha
validate :valid_commit_sha
def self.truncate_sha(sha)
@@ -36,12 +38,20 @@ module Ci
sha
end
+ def project
+ @project ||= gl_project.ensure_gitlab_ci_project
+ end
+
+ def project_id
+ project.id
+ end
+
def last_build
builds.order(:id).last
end
def retry
- builds_without_retry.each do |build|
+ latest_builds.each do |build|
Ci::Build.retry(build)
end
end
@@ -52,28 +62,16 @@ module Ci
end
end
- def new_branch?
- before_sha == Ci::Git::BLANK_SHA
- end
-
- def compare?
- !new_branch?
- end
-
def git_author_name
- commit_data[:author][:name] if commit_data && commit_data[:author]
+ commit_data.author_name if commit_data
end
def git_author_email
- commit_data[:author][:email] if commit_data && commit_data[:author]
+ commit_data.author_email if commit_data
end
def git_commit_message
- commit_data[:message] if commit_data && commit_data[:message]
- end
-
- def short_before_sha
- Ci::Commit.truncate_sha(before_sha)
+ commit_data.message if commit_data
end
def short_sha
@@ -81,127 +79,82 @@ module Ci
end
def commit_data
- push_data[:commits].find do |commit|
- commit[:id] == sha
- end
+ @commit ||= gl_project.commit(sha)
rescue
nil
end
- def project_recipients
- recipients = project.email_recipients.split(' ')
-
- if project.email_add_pusher? && push_data[:user_email].present?
- recipients << push_data[:user_email]
- end
-
- recipients.uniq
- end
-
def stage
- return unless config_processor
- stages = builds_without_retry.select(&:active?).map(&:stage)
- config_processor.stages.find { |stage| stages.include? stage }
+ running_or_pending = statuses.latest.running_or_pending.ordered
+ running_or_pending.first.try(:stage)
end
- def create_builds_for_stage(stage, trigger_request)
- return if skip_ci? && trigger_request.blank?
+ def create_builds(ref, tag, user, trigger_request = nil)
return unless config_processor
-
- builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag)
- builds_attrs.map do |build_attrs|
- builds.create!({
- project: project,
- name: build_attrs[:name],
- commands: build_attrs[:script],
- tag_list: build_attrs[:tags],
- options: build_attrs[:options],
- allow_failure: build_attrs[:allow_failure],
- stage: build_attrs[:stage],
- trigger_request: trigger_request,
- })
+ config_processor.stages.any? do |stage|
+ CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request, 'success').present?
end
end
- def create_next_builds(trigger_request)
- return if skip_ci? && trigger_request.blank?
+ def create_next_builds(build)
return unless config_processor
- stages = builds.where(trigger_request: trigger_request).group_by(&:stage)
+ # don't create other builds if this one is retried
+ latest_builds = builds.similar(build).latest
+ return unless latest_builds.exists?(build.id)
- config_processor.stages.any? do |stage|
- !stages.include?(stage) && create_builds_for_stage(stage, trigger_request).present?
- end
- end
+ # get list of stages after this build
+ next_stages = config_processor.stages.drop_while { |stage| stage != build.stage }
+ next_stages.delete(build.stage)
- def create_builds(trigger_request = nil)
- return if skip_ci? && trigger_request.blank?
- return unless config_processor
+ # get status for all prior builds
+ prior_builds = latest_builds.reject { |other_build| next_stages.include?(other_build.stage) }
+ status = Ci::Status.get_status(prior_builds)
- config_processor.stages.any? do |stage|
- create_builds_for_stage(stage, trigger_request).present?
+ # create builds for next stages based
+ next_stages.any? do |stage|
+ CreateBuildsService.new.execute(self, stage, build.ref, build.tag, build.user, build.trigger_request, status).present?
end
end
- def builds_without_retry
- @builds_without_retry ||=
- begin
- grouped_builds = builds.group_by(&:name)
- grouped_builds.map do |name, builds|
- builds.sort_by(&:id).last
- end
- end
+ def refs
+ statuses.order(:ref).pluck(:ref).uniq
end
- def builds_without_retry_sorted
- return builds_without_retry unless config_processor
+ def latest_statuses
+ @latest_statuses ||= statuses.latest.to_a
+ end
- stages = config_processor.stages
- builds_without_retry.sort_by do |build|
- [stages.index(build.stage) || -1, build.name || ""]
- end
+ def latest_builds
+ @latest_builds ||= builds.latest.to_a
+ end
+
+ def latest_builds_for_ref(ref)
+ latest_builds.select { |build| build.ref == ref }
end
- def retried_builds
- @retried_builds ||= (builds.order(id: :desc) - builds_without_retry)
+ def retried
+ @retried ||= (statuses.order(id: :desc) - statuses.latest)
end
def status
- if skip_ci?
- return 'skipped'
- elsif yaml_errors.present?
+ if yaml_errors.present?
return 'failed'
- elsif builds.none?
- return 'skipped'
- elsif success?
- 'success'
- elsif pending?
- 'pending'
- elsif running?
- 'running'
- elsif canceled?
- 'canceled'
- else
- 'failed'
end
+
+ @status ||= Ci::Status.get_status(latest_statuses)
end
def pending?
- builds_without_retry.all? do |build|
- build.pending?
- end
+ status == 'pending'
end
def running?
- builds_without_retry.any? do |build|
- build.running? || build.pending?
- end
+ status == 'running'
end
def success?
- builds_without_retry.all? do |build|
- build.success? || build.ignored?
- end
+ status == 'success'
end
def failed?
@@ -209,47 +162,50 @@ module Ci
end
def canceled?
- builds_without_retry.all? do |build|
- build.canceled?
- end
+ status == 'canceled'
end
def duration
- @duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i
+ duration_array = latest_statuses.map(&:duration).compact
+ duration_array.reduce(:+).to_i
end
def finished_at
- @finished_at ||= builds.order('finished_at DESC').first.try(:finished_at)
+ @finished_at ||= statuses.order('finished_at DESC').first.try(:finished_at)
end
def coverage
if project.coverage_enabled?
- coverage_array = builds_without_retry.map(&:coverage).compact
+ coverage_array = latest_builds.map(&:coverage).compact
if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
end
end
- def matrix?
- builds_without_retry.size > 1
+ def matrix_for_ref?(ref)
+ latest_builds_for_ref(ref).size > 1
end
def config_processor
- @config_processor ||= Ci::GitlabCiYamlProcessor.new(push_data[:ci_yaml_file])
- rescue Ci::GitlabCiYamlProcessor::ValidationError => e
+ return nil unless ci_yaml_file
+ @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, gl_project.path_with_namespace)
+ rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
save_yaml_error(e.message)
nil
- rescue Exception => e
- logger.error e.message + "\n" + e.backtrace.join("\n")
- save_yaml_error("Undefined yaml error")
+ rescue
+ save_yaml_error("Undefined error")
+ nil
+ end
+
+ def ci_yaml_file
+ gl_project.repository.blob_at(sha, '.gitlab-ci.yml').data
+ rescue
nil
end
def skip_ci?
- return false if builds.any?
- commits = push_data[:commits]
- commits.present? && commits.last[:message] =~ /(\[ci skip\])/
+ git_commit_message =~ /(\[ci skip\])/ if git_commit_message
end
def update_committed!
diff --git a/app/models/ci/event.rb b/app/models/ci/event.rb
index cac3a7a49c1..8c39be42677 100644
--- a/app/models/ci/event.rb
+++ b/app/models/ci/event.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: events
+# Table name: ci_events
#
# id :integer not null, primary key
# project_id :integer
diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb
index a52e28615f7..669ee1cc0d2 100644
--- a/app/models/ci/project.rb
+++ b/app/models/ci/project.rb
@@ -1,9 +1,9 @@
# == Schema Information
#
-# Table name: projects
+# Table name: ci_projects
#
# id :integer not null, primary key
-# name :string(255) not null
+# name :string(255)
# timeout :integer default(3600), not null
# created_at :datetime
# updated_at :datetime
@@ -33,15 +33,12 @@ module Ci
belongs_to :gl_project, class_name: '::Project', foreign_key: :gitlab_id
- has_many :commits, ->() { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }, dependent: :destroy, class_name: 'Ci::Commit'
- has_many :builds, through: :commits, dependent: :destroy, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, class_name: 'Ci::Runner'
has_many :web_hooks, dependent: :destroy, class_name: 'Ci::WebHook'
has_many :events, dependent: :destroy, class_name: 'Ci::Event'
has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
- has_one :last_commit, -> { order 'ci_commits.created_at DESC' }, class_name: 'Ci::Commit'
# Project services
has_many :services, dependent: :destroy, class_name: 'Ci::Service'
@@ -51,84 +48,65 @@ module Ci
accepts_nested_attributes_for :variables, allow_destroy: true
+ delegate :name_with_namespace, :path_with_namespace, :web_url, :http_url_to_repo, :ssh_url_to_repo, to: :gl_project
+
#
# Validations
#
- validates_presence_of :name, :timeout, :token, :default_ref,
- :path, :ssh_url_to_repo, :gitlab_id
+ validates_presence_of :timeout, :token, :default_ref, :gitlab_id
validates_uniqueness_of :gitlab_id
validates :polling_interval,
- presence: true,
- if: ->(project) { project.always_build.present? }
-
- scope :public_only, ->() { where(public: true) }
+ presence: true,
+ if: ->(project) { project.always_build.present? }
before_validation :set_default_values
class << self
include Ci::CurrentSettings
- def base_build_script
- <<-eos
- git submodule update --init
- ls -la
- eos
- end
-
- def parse(project)
- params = {
- name: project.name_with_namespace,
- gitlab_id: project.id,
- path: project.path_with_namespace,
- default_ref: project.default_branch || 'master',
- ssh_url_to_repo: project.ssh_url_to_repo,
- email_add_pusher: current_application_settings.add_pusher,
- email_only_broken_builds: current_application_settings.all_broken_builds,
- }
-
- project = Ci::Project.new(params)
- project.build_missing_services
- project
- end
-
- def already_added?(project)
- where(gitlab_id: project.id).any?
- end
-
def unassigned(runner)
joins("LEFT JOIN #{Ci::RunnerProject.table_name} ON #{Ci::RunnerProject.table_name}.project_id = #{Ci::Project.table_name}.id " \
"AND #{Ci::RunnerProject.table_name}.runner_id = #{runner.id}").
- where('#{Ci::RunnerProject.table_name}.project_id' => nil)
+ where("#{Ci::RunnerProject.table_name}.project_id" => nil)
end
def ordered_by_last_commit_date
- last_commit_subquery = "(SELECT project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY project_id)"
- joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.id = last_commit.project_id").
+ last_commit_subquery = "(SELECT gl_project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY gl_project_id)"
+ joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.gitlab_id = last_commit.gl_project_id").
+ joins(:gl_project).
order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC")
end
+ end
- def search(query)
- where("LOWER(#{Ci::Project.table_name}.name) LIKE :query",
- query: "%#{query.try(:downcase)}%")
- end
+ def name
+ name_with_namespace
+ end
+
+ def path
+ path_with_namespace
+ end
+
+ def gitlab_url
+ web_url
end
- def any_runners?
- if runners.active.any?
+ def any_runners?(&block)
+ if runners.active.any?(&block)
return true
end
- shared_runners_enabled && Ci::Runner.shared.active.any?
+ shared_runners_enabled && Ci::Runner.shared.active.any?(&block)
end
def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank?
+ self.default_ref ||= 'master'
end
def tracked_refs
- @tracked_refs ||= default_ref.split(",").map{|ref| ref.strip}
+ @tracked_refs ||= default_ref.split(",").map { |ref| ref.strip }
end
def valid_token? token
@@ -168,8 +146,7 @@ module Ci
# using http and basic auth
def repo_url_with_auth
auth = "gitlab-ci-token:#{token}@"
- url = gitlab_url + ".git"
- url.sub(/^https?:\/\//) do |prefix|
+ http_url_to_repo.sub(/^https?:\/\//) do |prefix|
prefix + auth
end
end
@@ -184,7 +161,7 @@ module Ci
# If service is available but missing in db
# we should create an instance. Ex `create_gitlab_ci_service`
- service = self.send :"create_#{service_name}_service" if service.nil?
+ self.send :"create_#{service_name}_service" if service.nil?
end
end
@@ -200,12 +177,16 @@ module Ci
end
end
- def gitlab_url
- File.join(Gitlab.config.gitlab.url, path)
- end
-
def setup_finished?
commits.any?
end
+
+ def commits
+ gl_project.ci_commits.ordered
+ end
+
+ def builds
+ gl_project.ci_builds
+ end
end
end
diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb
index 6d5cafe81a2..2d35aeac225 100644
--- a/app/models/ci/project_status.rb
+++ b/app/models/ci/project_status.rb
@@ -27,21 +27,5 @@ module Ci
def human_status
status
end
-
- # only check for toggling build status within same ref.
- def last_commit_changed_status?
- ref = last_commit.ref
- last_commits = commits.where(ref: ref).last(2)
-
- if last_commits.size < 2
- false
- else
- last_commits[0].status != last_commits[1].status
- end
- end
-
- def last_commit_for_ref(ref)
- commits.where(ref: ref).last
- end
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 1e9f78a3748..89710485811 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: runners
+# Table name: ci_runners
#
# id :integer not null, primary key
# token :string(255)
@@ -20,6 +20,8 @@
module Ci
class Runner < ActiveRecord::Base
extend Ci::Model
+
+ LAST_CONTACT_TIME = 5.minutes.ago
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
@@ -33,6 +35,8 @@ module Ci
scope :shared, ->() { where(is_shared: true) }
scope :active, ->() { where(active: true) }
scope :paused, ->() { where(active: false) }
+ scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
+ scope :ordered, ->() { order(id: :desc) }
acts_as_taggable
@@ -41,6 +45,10 @@ module Ci
query: "%#{query.try(:downcase)}%")
end
+ def gl_projects_ids
+ projects.select(:gitlab_id)
+ end
+
def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank?
end
@@ -52,7 +60,7 @@ module Ci
end
def display_name
- return token unless !description.blank?
+ return short_sha unless !description.blank?
description
end
@@ -61,6 +69,20 @@ module Ci
is_shared
end
+ def online?
+ contacted_at && contacted_at > LAST_CONTACT_TIME
+ end
+
+ def status
+ if contacted_at.nil?
+ :not_connected
+ elsif active?
+ online? ? :online : :offline
+ else
+ :paused
+ end
+ end
+
def belongs_to_one_project?
runner_projects.count == 1
end
@@ -74,7 +96,7 @@ module Ci
end
def short_sha
- token[0...10]
+ token[0...8] if token
end
end
end
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 44453ee4b41..3f4fc43873e 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: runner_projects
+# Table name: ci_runner_projects
#
# id :integer not null, primary key
# runner_id :integer not null
diff --git a/app/models/ci/service.rb b/app/models/ci/service.rb
index ed5e3f940b6..8063c51e82b 100644
--- a/app/models/ci/service.rb
+++ b/app/models/ci/service.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: services
+# Table name: ci_services
#
# id :integer not null, primary key
# type :string(255)
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index fe224b7dc70..b73c35d5ae5 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: triggers
+# Table name: ci_triggers
#
# id :integer not null, primary key
# token :string(255)
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index 29cd9553394..9973d2e5ade 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: trigger_requests
+# Table name: ci_trigger_requests
#
# id :integer not null, primary key
# trigger_id :integer not null
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 7a542802fa6..b3d2b809e03 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: variables
+# Table name: ci_variables
#
# id :integer not null, primary key
# project_id :integer not null
diff --git a/app/models/ci/web_hook.rb b/app/models/ci/web_hook.rb
index 8f03b0625da..0dc15eb6683 100644
--- a/app/models/ci/web_hook.rb
+++ b/app/models/ci/web_hook.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: web_hooks
+# Table name: ci_web_hooks
#
# id :integer not null, primary key
# url :string(255) not null
@@ -20,8 +20,7 @@ module Ci
# HTTParty timeout
default_timeout 10
- validates :url, presence: true,
- format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }
+ validates :url, presence: true, url: true
def execute(data)
parsed_url = URI.parse(url)
diff --git a/app/models/commit.rb b/app/models/commit.rb
index aff329d71fa..8ae5325d16a 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -2,13 +2,13 @@ class Commit
extend ActiveModel::Naming
include ActiveModel::Conversion
- include Mentionable
include Participable
+ include Mentionable
include Referable
include StaticModel
attr_mentionable :safe_message
- participant :author, :committer, :notes, :mentioned_users
+ participant :author, :committer, :notes
attr_accessor :project
@@ -78,11 +78,23 @@ class Commit
}x
end
+ def self.link_reference_pattern
+ super("commit", /(?<commit>\h{6,40})/)
+ end
+
def to_reference(from_project = nil)
if cross_project_reference?(from_project)
- "#{project.to_reference}@#{id}"
+ project.to_reference + self.class.reference_prefix + self.id
+ else
+ self.id
+ end
+ end
+
+ def reference_link_text(from_project = nil)
+ if cross_project_reference?(from_project)
+ project.to_reference + self.class.reference_prefix + self.short_id
else
- id
+ self.short_id
end
end
@@ -135,10 +147,10 @@ class Commit
description.present?
end
- def hook_attrs
+ def hook_attrs(with_changed_files: false)
path_with_namespace = project.path_with_namespace
- {
+ data = {
id: id,
message: safe_message,
timestamp: committed_date.xmlschema,
@@ -148,6 +160,12 @@ class Commit
email: author_email
}
}
+
+ if with_changed_files
+ data.merge!(repo_changes)
+ end
+
+ data
end
# Discover issues should be closed when this commit is pushed to a project's
@@ -164,6 +182,14 @@ class Commit
@committer ||= User.find_by_any_email(committer_email)
end
+ def parents
+ @parents ||= parent_ids.map { |id| project.commit(id) }
+ end
+
+ def parent
+ @parent ||= project.commit(self.parent_id) if self.parent_id
+ end
+
def notes
project.notes.for_commit_id(self.id)
end
@@ -181,7 +207,29 @@ class Commit
@raw.short_id(7)
end
- def parents
- @parents ||= Commit.decorate(super, project)
+ def ci_commit
+ project.ci_commit(sha)
+ end
+
+ def status
+ ci_commit.try(:status) || :not_found
+ end
+
+ private
+
+ def repo_changes
+ changes = { added: [], modified: [], removed: [] }
+
+ diffs.each do |diff|
+ if diff.deleted_file
+ changes[:removed] << diff.old_path
+ elsif diff.renamed_file || diff.new_file
+ changes[:added] << diff.new_path
+ else
+ changes[:modified] << diff.new_path
+ end
+ end
+
+ changes
end
end
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 86fc9eb01a3..14e7971fa06 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -2,36 +2,38 @@
#
# Examples:
#
-# range = CommitRange.new('f3f85602...e86e1013')
+# range = CommitRange.new('f3f85602...e86e1013', project)
# range.exclude_start? # => false
# range.reference_title # => "Commits f3f85602 through e86e1013"
# range.to_s # => "f3f85602...e86e1013"
#
-# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae')
+# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project)
# range.exclude_start? # => true
# range.reference_title # => "Commits f3f85602^ through e86e1013"
# range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"}
# range.to_s # => "f3f85602..e86e1013"
#
-# # Assuming `project` is a Project with a repository containing both commits:
-# range.project = project
+# # Assuming the specified project has a repository containing both commits:
# range.valid_commits? # => true
#
class CommitRange
include ActiveModel::Conversion
include Referable
- attr_reader :sha_from, :notation, :sha_to
+ attr_reader :commit_from, :notation, :commit_to
+ attr_reader :ref_from, :ref_to
# Optional Project model
attr_accessor :project
- # See `exclude_start?`
- attr_reader :exclude_start
-
- # The beginning and ending SHAs can be between 6 and 40 hex characters, and
+ # The beginning and ending refs can be named or SHAs, and
# the range notation can be double- or triple-dot.
- PATTERN = /\h{6,40}\.{2,3}\h{6,40}/
+ REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/
+ PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/
+
+ # In text references, the beginning and ending refs can only be SHAs
+ # between 6 and 40 hex characters.
+ STRICT_PATTERN = /\h{6,40}\.{2,3}\h{6,40}/
def self.reference_prefix
'@'
@@ -43,27 +45,40 @@ class CommitRange
def self.reference_pattern
%r{
(?:#{Project.reference_pattern}#{reference_prefix})?
- (?<commit_range>#{PATTERN})
+ (?<commit_range>#{STRICT_PATTERN})
}x
end
+ def self.link_reference_pattern
+ super("compare", /(?<commit_range>#{PATTERN})/)
+ end
+
# Initialize a CommitRange
#
# range_string - The String commit range.
# project - An optional Project model.
#
# Raises ArgumentError if `range_string` does not match `PATTERN`.
- def initialize(range_string, project = nil)
+ def initialize(range_string, project)
+ @project = project
+
range_string.strip!
- unless range_string.match(/\A#{PATTERN}\z/)
+ unless range_string =~ /\A#{PATTERN}\z/
raise ArgumentError, "invalid CommitRange string format: #{range_string}"
end
- @exclude_start = !range_string.include?('...')
- @sha_from, @notation, @sha_to = range_string.split(/(\.{2,3})/, 2)
+ @ref_from, @notation, @ref_to = range_string.split(/(\.{2,3})/, 2)
- @project = project
+ if project.valid_repo?
+ @commit_from = project.commit(@ref_from)
+ @commit_to = project.commit(@ref_to)
+ end
+
+ if valid_commits?
+ @ref_from = Commit.truncate_sha(sha_from) if sha_from.start_with?(@ref_from)
+ @ref_to = Commit.truncate_sha(sha_to) if sha_to.start_with?(@ref_to)
+ end
end
def inspect
@@ -71,15 +86,24 @@ class CommitRange
end
def to_s
- "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}"
+ sha_from + notation + sha_to
end
+ alias_method :id, :to_s
+
def to_reference(from_project = nil)
- # Not using to_s because we want the full SHAs
- reference = sha_from + notation + sha_to
+ if cross_project_reference?(from_project)
+ project.to_reference + self.class.reference_prefix + self.id
+ else
+ self.id
+ end
+ end
+
+ def reference_link_text(from_project = nil)
+ reference = ref_from + notation + ref_to
if cross_project_reference?(from_project)
- reference = project.to_reference + '@' + reference
+ reference = project.to_reference + self.class.reference_prefix + reference
end
reference
@@ -87,46 +111,58 @@ class CommitRange
# Returns a String for use in a link's title attribute
def reference_title
- "Commits #{suffixed_sha_from} through #{sha_to}"
+ "Commits #{sha_start} through #{sha_to}"
end
# Return a Hash of parameters for passing to a URL helper
#
# See `namespace_project_compare_url`
def to_param
- { from: suffixed_sha_from, to: sha_to }
+ { from: sha_start, to: sha_to }
end
def exclude_start?
- exclude_start
+ @notation == '..'
end
# Check if both the starting and ending commit IDs exist in a project's
# repository
- #
- # project - An optional Project to check (default: `project`)
- def valid_commits?(project = project)
- return nil unless project.present?
- return false unless project.valid_repo?
-
- commit_from.present? && commit_to.present?
+ def valid_commits?
+ commit_start.present? && commit_end.present?
end
def persisted?
true
end
- def commit_from
- @commit_from ||= project.repository.commit(suffixed_sha_from)
+ def sha_from
+ return nil unless @commit_from
+
+ @commit_from.id
+ end
+
+ def sha_to
+ return nil unless @commit_to
+
+ @commit_to.id
end
- def commit_to
- @commit_to ||= project.repository.commit(sha_to)
+ def sha_start
+ return nil unless sha_from
+
+ exclude_start? ? sha_from + '^' : sha_from
end
- private
+ def commit_start
+ return nil unless sha_start
- def suffixed_sha_from
- sha_from + (exclude_start? ? '^' : '')
+ if exclude_start?
+ @commit_start ||= project.commit(sha_start)
+ else
+ commit_from
+ end
end
+
+ alias_method :sha_end, :sha_to
+ alias_method :commit_end, :commit_to
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
new file mode 100644
index 00000000000..e70f4d37184
--- /dev/null
+++ b/app/models/commit_status.rb
@@ -0,0 +1,132 @@
+# == Schema Information
+#
+# Table name: ci_builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# coverage :float
+# commit_id :integer
+# commands :text
+# job_id :integer
+# name :string(255)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+# stage_idx :integer
+# tag :boolean
+# ref :string(255)
+# user_id :integer
+# type :string(255)
+# target_url :string(255)
+# description :string(255)
+# artifacts_file :text
+#
+
+class CommitStatus < ActiveRecord::Base
+ self.table_name = 'ci_builds'
+
+ belongs_to :commit, class_name: 'Ci::Commit'
+ belongs_to :user
+
+ validates :commit, presence: true
+ validates :status, inclusion: { in: %w(pending running failed success canceled) }
+
+ validates_presence_of :name
+
+ alias_attribute :author, :user
+
+ scope :running, -> { where(status: 'running') }
+ scope :pending, -> { where(status: 'pending') }
+ scope :success, -> { where(status: 'success') }
+ scope :failed, -> { where(status: 'failed') }
+ scope :running_or_pending, -> { where(status: [:running, :pending]) }
+ scope :finished, -> { where(status: [:success, :failed, :canceled]) }
+ scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) }
+ scope :ordered, -> { order(:ref, :stage_idx, :name) }
+ scope :for_ref, ->(ref) { where(ref: ref) }
+
+ state_machine :status, initial: :pending do
+ event :run do
+ transition pending: :running
+ end
+
+ event :drop do
+ transition [:pending, :running] => :failed
+ end
+
+ event :success do
+ transition [:pending, :running] => :success
+ end
+
+ event :cancel do
+ transition [:pending, :running] => :canceled
+ end
+
+ after_transition pending: :running do |build, transition|
+ build.update_attributes started_at: Time.now
+ end
+
+ after_transition any => [:success, :failed, :canceled] do |build, transition|
+ build.update_attributes finished_at: Time.now
+ end
+
+ state :pending, value: 'pending'
+ state :running, value: 'running'
+ state :failed, value: 'failed'
+ state :success, value: 'success'
+ state :canceled, value: 'canceled'
+ end
+
+ delegate :sha, :short_sha, :gl_project,
+ to: :commit, prefix: false
+
+ # TODO: this should be removed with all references
+ def before_sha
+ Gitlab::Git::BLANK_SHA
+ end
+
+ def started?
+ !pending? && !canceled? && started_at
+ end
+
+ def active?
+ running? || pending?
+ end
+
+ def complete?
+ canceled? || success? || failed?
+ end
+
+ def duration
+ if started_at && finished_at
+ finished_at - started_at
+ elsif started_at
+ Time.now - started_at
+ end
+ end
+
+ def cancel_url
+ nil
+ end
+
+ def retry_url
+ nil
+ end
+
+ def show_warning?
+ false
+ end
+
+ def download_url
+ nil
+ end
+end
diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb
new file mode 100644
index 00000000000..fe0cea8465f
--- /dev/null
+++ b/app/models/concerns/case_sensitivity.rb
@@ -0,0 +1,28 @@
+# Concern for querying columns with specific case sensitivity handling.
+module CaseSensitivity
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Queries the given columns regardless of the casing used.
+ #
+ # Unlike other ActiveRecord methods this method only operates on a Hash.
+ def iwhere(params)
+ criteria = self
+ cast_lower = Gitlab::Database.postgresql?
+
+ params.each do |key, value|
+ column = ActiveRecord::Base.connection.quote_table_name(key)
+
+ if cast_lower
+ condition = "LOWER(#{column}) = LOWER(:value)"
+ else
+ condition = "#{column} = :value"
+ end
+
+ criteria = criteria.where(condition, value: value)
+ end
+
+ criteria
+ end
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 4db4ffb2e79..badeadfa418 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -6,8 +6,9 @@
#
module Issuable
extend ActiveSupport::Concern
- include Mentionable
include Participable
+ include Mentionable
+ include StripAttribute
included do
belongs_to :author, class_name: "User"
@@ -24,7 +25,7 @@ module Issuable
scope :authored, ->(user) { where(author_id: user) }
scope :assigned_to, ->(u) { where(assignee_id: u.id)}
- scope :recent, -> { order("created_at DESC") }
+ scope :recent, -> { reorder(id: :desc) }
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) }
@@ -35,6 +36,9 @@ module Issuable
scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
+ scope :join_project, -> { joins(:project) }
+ scope :references_project, -> { references(:project) }
+
delegate :name,
:email,
to: :author,
@@ -47,7 +51,8 @@ module Issuable
prefix: true
attr_mentionable :title, :description
- participant :author, :assignee, :notes, :mentioned_users
+ participant :author, :assignee, :notes_with_associations
+ strip_attributes :title
end
module ClassMethods
@@ -85,39 +90,18 @@ module Issuable
assignee_id_changed?
end
- #
- # Votes
- #
-
- # Return the number of -1 comments (downvotes)
- def downvotes
- filter_superceded_votes(notes.select(&:downvote?), notes).size
+ def open?
+ opened? || reopened?
end
- def downvotes_in_percent
- if votes_count.zero?
- 0
- else
- 100.0 - upvotes_in_percent
- end
+ # Deprecated. Still exists to preserve API compatibility.
+ def downvotes
+ 0
end
- # Return the number of +1 comments (upvotes)
+ # Deprecated. Still exists to preserve API compatibility.
def upvotes
- filter_superceded_votes(notes.select(&:upvote?), notes).size
- end
-
- def upvotes_in_percent
- if votes_count.zero?
- 0
- else
- 100.0 / votes_count * upvotes
- end
- end
-
- # Return the total number of votes
- def votes_count
- upvotes + downvotes
+ 0
end
def subscribed?(user)
@@ -176,17 +160,12 @@ module Issuable
self.class.to_s.underscore
end
- private
-
- def filter_superceded_votes(votes, notes)
- filteredvotes = [] + votes
-
- votes.each do |vote|
- if vote.superceded?(notes)
- filteredvotes.delete(vote)
- end
- end
+ def notes_with_associations
+ notes.includes(:author, :project)
+ end
- filteredvotes
+ def updated_tasks
+ Taskable.get_updated_tasks(old_content: previous_changes['description'].first,
+ new_content: description)
end
end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 5b0ae411642..634a8d0f274 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -20,6 +20,12 @@ module Mentionable
end
end
+ included do
+ if self < Participable
+ participant ->(current_user) { mentioned_users(current_user, load_lazy_references: false) }
+ end
+ end
+
# Returns the text used as the body of a Note when this object is referenced
#
# By default this will be the class name and the result of calling
@@ -41,55 +47,54 @@ module Mentionable
self
end
- # Determine whether or not a cross-reference Note has already been created between this Mentionable and
- # the specified target.
- def has_mentioned?(target)
- SystemNoteService.cross_reference_exists?(target, local_reference)
+ def all_references(current_user = self.author, text = self.mentionable_text, load_lazy_references: true)
+ ext = Gitlab::ReferenceExtractor.new(self.project, current_user, load_lazy_references: load_lazy_references)
+ ext.analyze(text)
+ ext
end
- def mentioned_users(current_user = nil)
- return [] if mentionable_text.blank?
-
- ext = Gitlab::ReferenceExtractor.new(self.project, current_user)
- ext.analyze(mentionable_text)
- ext.users.uniq
+ def mentioned_users(current_user = nil, load_lazy_references: true)
+ all_references(current_user, load_lazy_references: load_lazy_references).users
end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
- def references(p = project, current_user = self.author, text = mentionable_text)
+ def referenced_mentionables(current_user = self.author, text = self.mentionable_text, load_lazy_references: true)
return [] if text.blank?
- ext = Gitlab::ReferenceExtractor.new(p, current_user)
- ext.analyze(text)
+ refs = all_references(current_user, text, load_lazy_references: load_lazy_references)
+ refs = (refs.issues + refs.merge_requests + refs.commits)
- (ext.issues + ext.merge_requests + ext.commits).uniq - [local_reference]
+ # We're using this method instead of Array diffing because that requires
+ # both of the object's `hash` values to be the same, which may not be the
+ # case for otherwise identical Commit objects.
+ refs.reject { |ref| ref == local_reference }
end
# Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+.
- def create_cross_references!(p = project, a = author, without = [])
- refs = references(p)
+ def create_cross_references!(author = self.author, without = [], text = self.mentionable_text)
+ refs = referenced_mentionables(author, text)
# We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the
# case for otherwise identical Commit objects.
- refs.reject! { |ref| without.include?(ref) }
+ refs.reject! { |ref| without.include?(ref) || cross_reference_exists?(ref) }
refs.each do |ref|
- SystemNoteService.cross_reference(ref, local_reference, a)
+ SystemNoteService.cross_reference(ref, local_reference, author)
end
end
# When a mentionable field is changed, creates cross-reference notes that
# don't already exist
- def create_new_cross_references!(p = project, a = author)
+ def create_new_cross_references!(author = self.author)
changes = detect_mentionable_changes
return if changes.empty?
original_text = changes.collect { |_, vals| vals.first }.join(' ')
- preexisting = references(p, self.author, original_text)
- create_cross_references!(p, a, preexisting)
+ preexisting = referenced_mentionables(author, original_text)
+ create_cross_references!(author, preexisting)
end
private
@@ -111,4 +116,10 @@ module Mentionable
# Only include changed fields that are mentionable
source.select { |key, val| mentionable.include?(key) }
end
+
+ # Determine whether or not a cross-reference Note has already been created between this Mentionable and
+ # the specified target.
+ def cross_reference_exists?(target)
+ SystemNoteService.cross_reference_exists?(target, local_reference)
+ end
end
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index 7c9597333dd..85367f89f4f 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -12,7 +12,7 @@
#
# # ...
#
-# participant :author, :assignee, :mentioned_users, :notes
+# participant :author, :assignee, :notes, ->(current_user) { mentioned_users(current_user) }
# end
#
# issue = Issue.last
@@ -27,7 +27,7 @@ module Participable
module ClassMethods
def participant(*attrs)
- participant_attrs.concat(attrs.map(&:to_s))
+ participant_attrs.concat(attrs)
end
def participant_attrs
@@ -37,21 +37,21 @@ module Participable
# Be aware that this method makes a lot of sql queries.
# Save result into variable if you are going to reuse it inside same request
- def participants(current_user = self.author, project = self.project)
+ def participants(current_user = self.author, load_lazy_references: true)
participants = self.class.participant_attrs.flat_map do |attr|
- meth = method(attr)
-
value =
- if meth.arity == 1 || meth.arity == -1
- meth.call(current_user)
+ if attr.respond_to?(:call)
+ instance_exec(current_user, &attr)
else
- meth.call
+ send(attr)
end
- participants_for(value, current_user, project)
+ participants_for(value, current_user)
end.compact.uniq
- if project
+ if load_lazy_references
+ participants = Gitlab::Markdown::ReferenceFilter::LazyReference.load(participants).uniq
+
participants.select! do |user|
user.can?(:read_project, project)
end
@@ -62,14 +62,14 @@ module Participable
private
- def participants_for(value, current_user = nil, project = nil)
+ def participants_for(value, current_user = nil)
case value
- when User
+ when User, Gitlab::Markdown::ReferenceFilter::LazyReference
[value]
when Enumerable, ActiveRecord::Relation
- value.flat_map { |v| participants_for(v, current_user, project) }
+ value.flat_map { |v| participants_for(v, current_user) }
when Participable
- value.participants(current_user, project)
+ value.participants(current_user, load_lazy_references: false)
end
end
end
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index cced66cc1e4..ce064f675ae 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -21,6 +21,10 @@ module Referable
''
end
+ def reference_link_text(from_project = nil)
+ to_reference(from_project)
+ end
+
module ClassMethods
# The character that prefixes the actual reference identifier
#
@@ -44,6 +48,25 @@ module Referable
def reference_pattern
raise NotImplementedError, "#{self} does not implement #{__method__}"
end
+
+ def link_reference_pattern(route, pattern)
+ %r{
+ (?<url>
+ #{Regexp.escape(Gitlab.config.gitlab.url)}
+ \/#{Project.reference_pattern}
+ \/#{Regexp.escape(route)}
+ \/#{pattern}
+ (?<path>
+ (\/[a-z0-9_=-]+)*
+ )?
+ (?<query>
+ \?[a-z0-9_=-]+
+ (&[a-z0-9_=-]+)*
+ )?
+ (?<anchor>\#[a-z0-9_-]+)?
+ )
+ }x
+ end
end
private
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 0ad2654867d..7391a77383c 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -8,12 +8,13 @@ module Sortable
included do
# By default all models should be ordered
# by created_at field starting from newest
- default_scope { order(created_at: :desc, id: :desc) }
+ default_scope { order_id_desc }
- scope :order_created_desc, -> { reorder(created_at: :desc, id: :desc) }
- scope :order_created_asc, -> { reorder(created_at: :asc, id: :asc) }
- scope :order_updated_desc, -> { reorder(updated_at: :desc, id: :desc) }
- scope :order_updated_asc, -> { reorder(updated_at: :asc, id: :asc) }
+ scope :order_id_desc, -> { reorder(id: :desc) }
+ scope :order_created_desc, -> { reorder(created_at: :desc) }
+ scope :order_created_asc, -> { reorder(created_at: :asc) }
+ scope :order_updated_desc, -> { reorder(updated_at: :desc) }
+ scope :order_updated_asc, -> { reorder(updated_at: :asc) }
scope :order_name_asc, -> { reorder(name: :asc) }
scope :order_name_desc, -> { reorder(name: :desc) }
end
diff --git a/app/models/concerns/strip_attribute.rb b/app/models/concerns/strip_attribute.rb
new file mode 100644
index 00000000000..8806ebe897a
--- /dev/null
+++ b/app/models/concerns/strip_attribute.rb
@@ -0,0 +1,34 @@
+# == Strip Attribute module
+#
+# Contains functionality to clean attributes before validation
+#
+# Usage:
+#
+# class Milestone < ActiveRecord::Base
+# strip_attributes :title
+# end
+#
+#
+module StripAttribute
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def strip_attributes(*attrs)
+ strip_attrs.concat(attrs)
+ end
+
+ def strip_attrs
+ @strip_attrs ||= []
+ end
+ end
+
+ included do
+ before_validation :strip_attributes
+ end
+
+ def strip_attributes
+ self.class.strip_attrs.each do |attr|
+ self[attr].strip! if self[attr] && self[attr].respond_to?(:strip!)
+ end
+ end
+end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index 660e58b876d..df2a9e3e84b 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -7,14 +7,39 @@ require 'task_list/filter'
#
# Used by MergeRequest and Issue
module Taskable
+ COMPLETED = 'completed'.freeze
+ INCOMPLETE = 'incomplete'.freeze
+ ITEM_PATTERN = /
+ ^
+ (?:\s*[-+*]|(?:\d+\.))? # optional list prefix
+ \s* # optional whitespace prefix
+ (\[\s\]|\[[xX]\]) # checkbox
+ (\s.+) # followed by whitespace and some text.
+ /x
+
+ def self.get_tasks(content)
+ content.to_s.scan(ITEM_PATTERN).map do |checkbox, label|
+ # ITEM_PATTERN strips out the hyphen, but Item requires it. Rabble rabble.
+ TaskList::Item.new("- #{checkbox}", label.strip)
+ end
+ end
+
+ def self.get_updated_tasks(old_content:, new_content:)
+ old_tasks, new_tasks = get_tasks(old_content), get_tasks(new_content)
+
+ new_tasks.select.with_index do |new_task, i|
+ old_task = old_tasks[i]
+ next unless old_task
+
+ new_task.source == old_task.source && new_task.complete? != old_task.complete?
+ end
+ end
+
# Called by `TaskList::Summary`
def task_list_items
return [] if description.blank?
- @task_list_items ||= description.scan(TaskList::Filter::ItemPattern).collect do |item|
- # ItemPattern strips out the hyphen, but Item requires it. Rabble rabble.
- TaskList::Item.new("- #{item}")
- end
+ @task_list_items ||= Taskable.get_tasks(description)
end
def tasks
diff --git a/app/models/event.rb b/app/models/event.rb
index 47600c57e35..01d008035a5 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -45,7 +45,7 @@ class Event < ActiveRecord::Base
after_create :reset_project_activity
# Scopes
- scope :recent, -> { order(created_at: :desc) }
+ scope :recent, -> { reorder(id: :desc) }
scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent }
scope :with_associations, -> { includes(project: :namespace) }
@@ -63,6 +63,16 @@ class Event < ActiveRecord::Base
Event::PUSHED, ["MergeRequest", "Issue"],
[Event::CREATED, Event::CLOSED, Event::MERGED])
end
+
+ def latest_update_time
+ row = select(:updated_at, :project_id).reorder(id: :desc).take
+
+ row ? row.updated_at : nil
+ end
+
+ def limit_recent(limit = 20, offset = nil)
+ recent.limit(limit).offset(offset)
+ end
end
def proper?
@@ -191,7 +201,7 @@ class Event < ActiveRecord::Base
elsif commented?
"commented on"
elsif created_project?
- if project.import?
+ if project.external_import?
"imported"
else
"created"
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
new file mode 100644
index 00000000000..12c934e2494
--- /dev/null
+++ b/app/models/generic_commit_status.rb
@@ -0,0 +1,48 @@
+# == Schema Information
+#
+# Table name: ci_builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# coverage :float
+# commit_id :integer
+# commands :text
+# job_id :integer
+# name :string(255)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+# stage_idx :integer
+# tag :boolean
+# ref :string(255)
+# user_id :integer
+# type :string(255)
+# target_url :string(255)
+# description :string(255)
+# artifacts_file :text
+#
+
+class GenericCommitStatus < CommitStatus
+ before_validation :set_default_values
+
+ # GitHub compatible API
+ alias_attribute :context, :name
+
+ def set_default_values
+ self.context ||= 'default'
+ self.stage ||= 'external'
+ end
+
+ def tags
+ [:external]
+ end
+end
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
new file mode 100644
index 00000000000..0171f7d54b7
--- /dev/null
+++ b/app/models/global_label.rb
@@ -0,0 +1,17 @@
+class GlobalLabel
+ attr_accessor :title, :labels
+ alias_attribute :name, :title
+
+ def self.build_collection(labels)
+ labels = labels.group_by(&:title)
+
+ labels.map do |title, label|
+ new(title, label)
+ end
+ end
+
+ def initialize(title, labels)
+ @title = title
+ @labels = labels
+ end
+end
diff --git a/app/models/group_milestone.rb b/app/models/global_milestone.rb
index ab055f6b80b..8bfc79d88f8 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -1,20 +1,30 @@
-class GroupMilestone
+class GlobalMilestone
+ attr_accessor :title, :milestones
+ alias_attribute :name, :title
+
+ def self.build_collection(milestones)
+ milestones = milestones.group_by(&:title)
+
+ milestones.map do |title, milestones|
+ new(title, milestones)
+ end
+ end
def initialize(title, milestones)
@title = title
@milestones = milestones
end
- def title
- @title
- end
-
def safe_title
- @title.parameterize
+ @title.to_slug.to_s
end
- def milestones
- @milestones
+ def expired?
+ if due_date
+ due_date.past?
+ else
+ false
+ end
end
def projects
@@ -66,15 +76,15 @@ class GroupMilestone
end
def issues
- @group_issues ||= milestones.map(&:issues).flatten.group_by(&:state)
+ @issues ||= milestones.map(&:issues).flatten.group_by(&:state)
end
def merge_requests
- @group_merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
+ @merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
end
def participants
- @group_participants ||= milestones.map(&:participants).flatten.compact.uniq
+ @participants ||= milestones.map(&:participants).flatten.compact.uniq
end
def opened_issues
@@ -92,4 +102,29 @@ class GroupMilestone
def closed_merge_requests
merge_requests.values_at("closed", "merged", "locked").compact.flatten
end
+
+ def complete?
+ total_items_count == closed_items_count
+ end
+
+ def due_date
+ return @due_date if defined?(@due_date)
+
+ @due_date =
+ if @milestones.all? { |x| x.due_date == @milestones.first.due_date }
+ @milestones.first.due_date
+ else
+ nil
+ end
+ end
+
+ def expires_at
+ if due_date
+ if due_date.past?
+ "expired at #{due_date.stamp("Aug 21, 2011")}"
+ else
+ "expires at #{due_date.stamp("Aug 21, 2011")}"
+ end
+ end
+ end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 9cd146bb73b..1b5b875a19e 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -11,6 +11,7 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
+# public :boolean default(FALSE)
#
require 'carrierwave/orm/activerecord'
@@ -19,8 +20,9 @@ require 'file_size_validator'
class Group < Namespace
include Gitlab::ConfigHelper
include Referable
-
+
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
+ alias_method :members, :group_members
has_many :users, through: :group_members
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
@@ -47,6 +49,14 @@ class Group < Namespace
def reference_pattern
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
end
def to_reference(_from_project = nil)
@@ -64,7 +74,7 @@ class Group < Namespace
end
def owners
- @owners ||= group_members.owners.map(&:user)
+ @owners ||= group_members.owners.includes(:user).map(&:user)
end
def add_users(user_ids, access_level, current_user = nil)
@@ -109,10 +119,6 @@ class Group < Namespace
has_owner?(user) && owners.size == 1
end
- def members
- group_members
- end
-
def avatar_type
unless self.avatar.image?
self.errors.add :avatar, "only images allowed"
@@ -120,7 +126,7 @@ class Group < Namespace
end
def public_profile?
- projects.public_only.any?
+ self.public || projects.public_only.any?
end
def post_create_hook
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index ca7066b959a..337b3097126 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -2,18 +2,19 @@
#
# Table name: web_hooks
#
-# id :integer not null, primary key
-# url :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255) default("ProjectHook")
-# service_id :integer
-# push_events :boolean default(TRUE), not null
-# issues_events :boolean default(FALSE), not null
-# merge_requests_events :boolean default(FALSE), not null
-# tag_push_events :boolean default(FALSE)
-# note_events :boolean default(FALSE), not null
+# id :integer not null, primary key
+# url :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# type :string(255) default("ProjectHook")
+# service_id :integer
+# push_events :boolean default(TRUE), not null
+# issues_events :boolean default(FALSE), not null
+# merge_requests_events :boolean default(FALSE), not null
+# tag_push_events :boolean default(FALSE)
+# note_events :boolean default(FALSE), not null
+# enable_ssl_verification :boolean default(TRUE)
#
class ProjectHook < WebHook
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index b55e217975f..09bb3ee52a2 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -2,18 +2,19 @@
#
# Table name: web_hooks
#
-# id :integer not null, primary key
-# url :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255) default("ProjectHook")
-# service_id :integer
-# push_events :boolean default(TRUE), not null
-# issues_events :boolean default(FALSE), not null
-# merge_requests_events :boolean default(FALSE), not null
-# tag_push_events :boolean default(FALSE)
-# note_events :boolean default(FALSE), not null
+# id :integer not null, primary key
+# url :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# type :string(255) default("ProjectHook")
+# service_id :integer
+# push_events :boolean default(TRUE), not null
+# issues_events :boolean default(FALSE), not null
+# merge_requests_events :boolean default(FALSE), not null
+# tag_push_events :boolean default(FALSE)
+# note_events :boolean default(FALSE), not null
+# enable_ssl_verification :boolean default(TRUE)
#
class ServiceHook < WebHook
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index 6fb2d421026..2f63c59b07e 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -2,18 +2,19 @@
#
# Table name: web_hooks
#
-# id :integer not null, primary key
-# url :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255) default("ProjectHook")
-# service_id :integer
-# push_events :boolean default(TRUE), not null
-# issues_events :boolean default(FALSE), not null
-# merge_requests_events :boolean default(FALSE), not null
-# tag_push_events :boolean default(FALSE)
-# note_events :boolean default(FALSE), not null
+# id :integer not null, primary key
+# url :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# type :string(255) default("ProjectHook")
+# service_id :integer
+# push_events :boolean default(TRUE), not null
+# issues_events :boolean default(FALSE), not null
+# merge_requests_events :boolean default(FALSE), not null
+# tag_push_events :boolean default(FALSE)
+# note_events :boolean default(FALSE), not null
+# enable_ssl_verification :boolean default(TRUE)
#
class SystemHook < WebHook
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index a078accbdbd..715ec5908b7 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -2,18 +2,19 @@
#
# Table name: web_hooks
#
-# id :integer not null, primary key
-# url :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255) default("ProjectHook")
-# service_id :integer
-# push_events :boolean default(TRUE), not null
-# issues_events :boolean default(FALSE), not null
-# merge_requests_events :boolean default(FALSE), not null
-# tag_push_events :boolean default(FALSE)
-# note_events :boolean default(FALSE), not null
+# id :integer not null, primary key
+# url :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# type :string(255) default("ProjectHook")
+# service_id :integer
+# push_events :boolean default(TRUE), not null
+# issues_events :boolean default(FALSE), not null
+# merge_requests_events :boolean default(FALSE), not null
+# tag_push_events :boolean default(FALSE)
+# note_events :boolean default(FALSE), not null
+# enable_ssl_verification :boolean default(TRUE)
#
class WebHook < ActiveRecord::Base
@@ -30,37 +31,38 @@ class WebHook < ActiveRecord::Base
# HTTParty timeout
default_timeout Gitlab.config.gitlab.webhook_timeout
- validates :url, presence: true,
- format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }
+ validates :url, presence: true, url: true
def execute(data, hook_name)
parsed_url = URI.parse(url)
if parsed_url.userinfo.blank?
- WebHook.post(url,
- body: data.to_json,
- headers: {
- "Content-Type" => "application/json",
- "X-Gitlab-Event" => hook_name.singularize.titleize
- },
- verify: enable_ssl_verification)
+ response = WebHook.post(url,
+ body: data.to_json,
+ headers: {
+ "Content-Type" => "application/json",
+ "X-Gitlab-Event" => hook_name.singularize.titleize
+ },
+ verify: enable_ssl_verification)
else
post_url = url.gsub("#{parsed_url.userinfo}@", "")
auth = {
username: URI.decode(parsed_url.user),
password: URI.decode(parsed_url.password),
}
- WebHook.post(post_url,
- body: data.to_json,
- headers: {
- "Content-Type" => "application/json",
- "X-Gitlab-Event" => hook_name.singularize.titleize
- },
- verify: enable_ssl_verification,
- basic_auth: auth)
+ response = WebHook.post(post_url,
+ body: data.to_json,
+ headers: {
+ "Content-Type" => "application/json",
+ "X-Gitlab-Event" => hook_name.singularize.titleize
+ },
+ verify: enable_ssl_verification,
+ basic_auth: auth)
end
- rescue SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
+
+ [response.code == 200, ActionView::Base.full_sanitizer.sanitize(response.to_s)]
+ rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
logger.error("WebHook Error => #{e}")
- false
+ [false, e.to_s]
end
def async_execute(data, hook_name)
diff --git a/app/models/issue.rb b/app/models/issue.rb
index fc7e9abe29e..187b6482b6c 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -69,6 +69,10 @@ class Issue < ActiveRecord::Base
}x
end
+ def self.link_reference_pattern
+ super("issues", /(?<issue>\d+)/)
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -95,4 +99,14 @@ class Issue < ActiveRecord::Base
def source_project
project
end
+
+ # From all notes on this issue, we'll select the system notes about linked
+ # merge requests. Of those, the MRs closing `self` are returned.
+ def closed_by_merge_requests(current_user = nil)
+ return [] unless open?
+
+ notes.system.flat_map do |note|
+ note.all_references(current_user).merge_requests
+ end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
+ end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 4a22bd53400..220da10a6ab 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -8,10 +8,16 @@
# project_id :integer
# created_at :datetime
# updated_at :datetime
+# template :boolean default(FALSE)
#
class Label < ActiveRecord::Base
include Referable
+ # Represents a "No Label" state used for filtering Issues and Merge
+ # Requests that have no label assigned.
+ LabelStruct = Struct.new(:title, :name)
+ None = LabelStruct.new('No Label', 'No Label')
+ Any = LabelStruct.new('Any Label', '')
DEFAULT_COLOR = '#428BCA'
@@ -21,9 +27,7 @@ class Label < ActiveRecord::Base
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
- validates :color,
- format: { with: /\A#[0-9A-Fa-f]{6}\Z/ },
- allow_blank: false
+ validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow '?', '&', and ',' for label titles
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
new file mode 100644
index 00000000000..3c1426f59d0
--- /dev/null
+++ b/app/models/lfs_object.rb
@@ -0,0 +1,8 @@
+class LfsObject < ActiveRecord::Base
+ has_many :lfs_objects_projects, dependent: :destroy
+ has_many :projects, through: :lfs_objects_projects
+
+ validates :oid, presence: true, uniqueness: true
+
+ mount_uploader :file, LfsObjectUploader
+end
diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb
new file mode 100644
index 00000000000..0fd5f089db9
--- /dev/null
+++ b/app/models/lfs_objects_project.rb
@@ -0,0 +1,8 @@
+class LfsObjectsProject < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :lfs_object
+
+ validates :lfs_object_id, presence: true
+ validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
+ validates :project_id, presence: true
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index cae8caa23fb..28aee2e3799 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -30,13 +30,22 @@ class Member < ActiveRecord::Base
validates :user, presence: true, unless: :invite?
validates :source, presence: true
- validates :user_id, uniqueness: { scope: [:source_type, :source_id],
+ validates :user_id, uniqueness: { scope: [:source_type, :source_id],
message: "already exists in source",
allow_nil: true }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
- validates :invite_email, presence: { if: :invite? },
- email: { strict_mode: true, allow_nil: true },
- uniqueness: { scope: [:source_type, :source_id], allow_nil: true }
+ validates :invite_email,
+ presence: {
+ if: :invite?
+ },
+ email: {
+ strict_mode: true,
+ allow_nil: true
+ },
+ uniqueness: {
+ scope: [:source_type, :source_id],
+ allow_nil: true
+ }
scope :invite, -> { where(user_id: nil) }
scope :non_invite, -> { where("user_id IS NOT NULL") }
@@ -73,7 +82,7 @@ class Member < ActiveRecord::Base
def add_user(members, user_id, access_level, current_user = nil)
user = user_for_id(user_id)
-
+
# `user` can be either a User object or an email to be invited
if user.is_a?(User)
member = members.find_or_initialize_by(user_id: user.id)
@@ -82,10 +91,21 @@ class Member < ActiveRecord::Base
member.invite_email = user
end
- member.created_by ||= current_user
- member.access_level = access_level
+ if can_update_member?(current_user, member)
+ member.created_by ||= current_user
+ member.access_level = access_level
+
+ member.save
+ end
+ end
+
+ private
- member.save
+ def can_update_member?(current_user, member)
+ # There is no current user for bulk actions, in which case anything is allowed
+ !current_user ||
+ current_user.can?(:update_group_member, member) ||
+ current_user.can?(:update_project_member, member)
end
end
@@ -95,7 +115,7 @@ class Member < ActiveRecord::Base
def accept_invite!(new_user)
return false unless invite?
-
+
self.invite_token = nil
self.invite_accepted_at = Time.now.utc
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index eb468c6cd53..080b7f7fb88 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -20,6 +20,7 @@
# position :integer default(0)
# locked_at :datetime
# updated_by_id :integer
+# merge_error :string(255)
#
require Rails.root.join("app/models/commit")
@@ -40,7 +41,7 @@ class MergeRequest < ActiveRecord::Base
after_create :create_merge_request_diff
after_update :update_merge_request_diff
- delegate :commits, :diffs, :last_commit, :last_commit_short_sha, to: :merge_request_diff, prefix: nil
+ delegate :commits, :diffs, :diffs_no_whitespace, to: :merge_request_diff, prefix: nil
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -133,6 +134,9 @@ class MergeRequest < ActiveRecord::Base
scope :closed, -> { with_state(:closed) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
+ scope :join_project, -> { joins(:target_project) }
+ scope :references_project, -> { references(:target_project) }
+
def self.reference_prefix
'!'
end
@@ -147,6 +151,10 @@ class MergeRequest < ActiveRecord::Base
}x
end
+ def self.link_reference_pattern
+ super("merge_requests", /(?<merge_request>\d+)/)
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -157,6 +165,18 @@ class MergeRequest < ActiveRecord::Base
reference
end
+ def last_commit
+ merge_request_diff ? merge_request_diff.last_commit : compare_commits.last
+ end
+
+ def first_commit
+ merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
+ end
+
+ def last_commit_short_sha
+ last_commit.short_id
+ end
+
def validate_branches
if target_project == source_project && target_branch == source_branch
errors.add :branch_conflict, "You can not use same project/branch for source and target"
@@ -222,12 +242,8 @@ class MergeRequest < ActiveRecord::Base
self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
end
- def open?
- opened? || reopened?
- end
-
def work_in_progress?
- title =~ /\A\[?WIP\]?:? /i
+ !!(title =~ /\A\[?WIP\]?:? /i)
end
def mergeable?
@@ -249,7 +265,7 @@ class MergeRequest < ActiveRecord::Base
Note.where(
"(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
- "(project_id = :source_project_id AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
+ "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
mr_id: id,
commit_ids: commit_ids,
target_project_id: target_project_id,
@@ -275,10 +291,11 @@ class MergeRequest < ActiveRecord::Base
attrs = {
source: source_project.hook_attrs,
target: target_project.hook_attrs,
- last_commit: nil
+ last_commit: nil,
+ work_in_progress: work_in_progress?
}
- unless last_commit.nil?
+ if last_commit
attrs.merge!(last_commit: last_commit.hook_attrs)
end
@@ -293,13 +310,17 @@ class MergeRequest < ActiveRecord::Base
target_project
end
+ def closes_issue?(issue)
+ closes_issues.include?(issue)
+ end
+
# 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.sort_by(&:id)
+ issues.uniq
else
[]
end
@@ -457,4 +478,10 @@ class MergeRequest < ActiveRecord::Base
unlock_mr if locked?
end
end
+
+ def ci_commit
+ if last_commit and source_project
+ source_project.ci_commit(last_commit.id)
+ end
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index f75f999b0d0..c499a4b5b4c 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -19,7 +19,7 @@ class MergeRequestDiff < ActiveRecord::Base
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 500
- attr_reader :commits, :diffs
+ attr_reader :commits, :diffs, :diffs_no_whitespace
belongs_to :merge_request
@@ -47,6 +47,20 @@ class MergeRequestDiff < ActiveRecord::Base
@diffs ||= (load_diffs(st_diffs) || [])
end
+ def diffs_no_whitespace
+ # Get latest sha of branch from source project
+ source_sha = merge_request.source_project.commit(source_branch).sha
+
+ compare_result = Gitlab::CompareResult.new(
+ Gitlab::Git::Compare.new(
+ merge_request.target_project.repository.raw_repository,
+ merge_request.target_branch,
+ source_sha,
+ ), { ignore_whitespace_change: true }
+ )
+ @diffs_no_whitespace ||= load_diffs(dump_commits(compare_result.diffs))
+ end
+
def commits
@commits ||= load_commits(st_commits || [])
end
@@ -55,6 +69,10 @@ class MergeRequestDiff < ActiveRecord::Base
commits.first
end
+ def first_commit
+ commits.last
+ end
+
def last_commit_short_sha
@last_commit_short_sha ||= last_commit.short_id
end
@@ -144,12 +162,10 @@ class MergeRequestDiff < ActiveRecord::Base
# Collect array of Git::Diff objects
# between target and source branches
def unmerged_diffs
- diffs = compare_result.diffs
- diffs ||= []
- diffs
- rescue Gitlab::Git::Diff::TimeoutError => ex
+ compare_result.diffs || []
+ rescue Gitlab::Git::Diff::TimeoutError
self.state = :timeout
- diffs = []
+ []
end
def repository
@@ -165,7 +181,8 @@ class MergeRequestDiff < ActiveRecord::Base
merge_request.fetch_ref
# Get latest sha of branch from source project
- source_sha = merge_request.source_project.commit(source_branch).sha
+ source_commit = merge_request.source_project.commit(source_branch)
+ source_sha = source_commit.try(:sha)
Gitlab::CompareResult.new(
Gitlab::Git::Compare.new(
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index d979a35084b..d8c7536cd31 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -16,10 +16,13 @@
class Milestone < ActiveRecord::Base
# Represents a "No Milestone" state used for filtering Issues and Merge
# Requests that have no milestone assigned.
- None = Struct.new(:title).new('No Milestone')
+ MilestoneStruct = Struct.new(:title, :name, :id)
+ None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
+ Any = MilestoneStruct.new('Any Milestone', '', -1)
include InternalId
include Sortable
+ include StripAttribute
belongs_to :project
has_many :issues
@@ -33,6 +36,8 @@ class Milestone < ActiveRecord::Base
validates :title, presence: true
validates :project, presence: true
+ strip_attributes :title
+
state_machine :state, initial: :active do
event :close do
transition active: :closed
@@ -47,6 +52,8 @@ class Milestone < ActiveRecord::Base
state :active
end
+ alias_attribute :name, :title
+
class << self
def search(query)
query = "%#{query}%"
@@ -101,4 +108,36 @@ class Milestone < ActiveRecord::Base
def author_id
nil
end
+
+ # Sorts the issues for the given IDs.
+ #
+ # This method runs a single SQL query using a CASE statement to update the
+ # position of all issues in the current milestone (scoped to the list of IDs).
+ #
+ # Given the ids [10, 20, 30] this method produces a SQL query something like
+ # the following:
+ #
+ # UPDATE issues
+ # SET position = CASE
+ # WHEN id = 10 THEN 1
+ # WHEN id = 20 THEN 2
+ # WHEN id = 30 THEN 3
+ # ELSE position
+ # END
+ # WHERE id IN (10, 20, 30);
+ #
+ # This method expects that the IDs given in `ids` are already Fixnums.
+ def sort_issues(ids)
+ pairs = []
+
+ ids.each_with_index do |id, index|
+ pairs << id
+ pairs << index + 1
+ end
+
+ conditions = 'WHEN id = ? THEN ? ' * ids.length
+
+ issues.where(id: ids).
+ update_all(["position = CASE #{conditions} ELSE position END", *pairs])
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 161a16ca61c..1c4e101cc10 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -11,6 +11,7 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
+# public :boolean default(FALSE)
#
class Namespace < ActiveRecord::Base
@@ -22,19 +23,17 @@ class Namespace < ActiveRecord::Base
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
- presence: true, uniqueness: true,
length: { within: 0..255 },
- format: { with: Gitlab::Regex.namespace_name_regex,
- message: Gitlab::Regex.namespace_name_regex_message }
+ namespace_name: true,
+ presence: true,
+ uniqueness: true
validates :description, length: { within: 0..255 }
validates :path,
- uniqueness: { case_sensitive: false },
- presence: true,
length: { within: 1..255 },
- exclusion: { in: Gitlab::Blacklist.path },
- format: { with: Gitlab::Regex.namespace_regex,
- message: Gitlab::Regex.namespace_regex_message }
+ namespace: true,
+ presence: true,
+ uniqueness: { case_sensitive: false }
delegate :name, to: :owner, allow_nil: true, prefix: true
@@ -118,6 +117,8 @@ class Namespace < ActiveRecord::Base
gitlab_shell.add_namespace(path_was)
if gitlab_shell.mv_namespace(path_was, path)
+ Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+
# If repositories moved successfully we need to
# send update instructions to users.
# However we cannot allow rollback since we moved namespace dir
@@ -137,7 +138,9 @@ class Namespace < ActiveRecord::Base
end
def send_update_instructions
- projects.each(&:send_move_instructions)
+ projects.each do |project|
+ project.send_move_instructions("#{path_was}/#{project.path}")
+ end
end
def kind
diff --git a/app/models/note.rb b/app/models/note.rb
index 89d81ab1de2..8d433c57ceb 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -22,14 +22,14 @@ require 'carrierwave/orm/activerecord'
require 'file_size_validator'
class Note < ActiveRecord::Base
- include Mentionable
include Gitlab::CurrentSettings
include Participable
+ include Mentionable
default_value_for :system, false
attr_mentionable :note
- participant :author, :mentioned_users
+ participant :author
belongs_to :project
belongs_to :noteable, polymorphic: true
@@ -39,17 +39,24 @@ class Note < ActiveRecord::Base
delegate :name, to: :project, prefix: true
delegate :name, :email, to: :author, prefix: true
+ before_validation :set_award!
+
validates :note, :project, presence: true
- validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
+ validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
+ validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award }
+ validates :line_code, line_code: true, allow_blank: true
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' }
validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' }
+ validates :author, presence: true
mount_uploader :attachment, AttachmentUploader
# Scopes
+ scope :awards, ->{ where(is_award: true) }
+ scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") }
scope :not_inline, ->{ where(line_code: [nil, '']) }
@@ -60,9 +67,13 @@ class Note < ActiveRecord::Base
scope :inc_author_project, ->{ includes(:project, :author) }
scope :inc_author, ->{ includes(:author) }
+ scope :with_associations, -> do
+ includes(:author, :noteable, :updated_by,
+ project: [:project_members, { group: [:group_members] }])
+ end
+
serialize :st_diff
before_create :set_diff, if: ->(n) { n.line_code.present? }
- after_update :set_references
class << self
def discussions_from_notes(notes)
@@ -93,6 +104,12 @@ class Note < ActiveRecord::Base
def search(query)
where("LOWER(note) like :query", query: "%#{query.downcase}%")
end
+
+ def grouped_awards
+ awards.select(:note).distinct.map do |note|
+ [ note.note, where(note: note.note) ]
+ end
+ end
end
def cross_reference?
@@ -284,44 +301,6 @@ class Note < ActiveRecord::Base
nil
end
- DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:)
-
- # Check if the note is a downvote
- def downvote?
- votable? && note.start_with?(*DOWNVOTES)
- end
-
- UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:)
-
- # Check if the note is an upvote
- def upvote?
- votable? && note.start_with?(*UPVOTES)
- end
-
- def superceded?(notes)
- return false unless vote?
-
- notes.each do |note|
- next if note == self
-
- if note.vote? &&
- self[:author_id] == note[:author_id] &&
- self[:created_at] <= note[:created_at]
- return true
- end
- end
-
- false
- end
-
- def vote?
- upvote? || downvote?
- end
-
- def votable?
- for_issue? || (for_merge_request? && !for_diff_line?)
- end
-
# Mentionable override.
def gfm_reference(from_project = nil)
noteable.gfm_reference(from_project)
@@ -333,15 +312,13 @@ class Note < ActiveRecord::Base
end
def noteable_type_name
- if noteable_type.present?
- noteable_type.downcase
- end
+ 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=(sType)
- super(sType.to_s.classify.constantize.base_class.to_s)
+ def noteable_type=(noteable_type)
+ super(noteable_type.to_s.classify.constantize.base_class.to_s)
end
# Reset notes events cache
@@ -357,15 +334,48 @@ class Note < ActiveRecord::Base
Event.reset_event_cache_for(self)
end
- def set_references
- create_new_cross_references!(project, author)
- end
-
def system?
read_attribute(:system)
end
+ # Deprecated. Still exists to preserve API compatibility.
+ def downvote?
+ false
+ end
+
+ # Deprecated. Still exists to preserve API compatibility.
+ def upvote?
+ false
+ end
+
def editable?
- !read_attribute(:system)
+ !system?
+ end
+
+ # Checks if note is an award added as a comment
+ #
+ # If note is an award, this method sets is_award to true
+ # and changes content of the note to award name.
+ #
+ # Method is executed as a before_validation callback.
+ #
+ def set_award!
+ return unless awards_supported? && contains_emoji_only?
+ self.is_award = true
+ self.note = award_emoji_name
+ end
+
+ private
+
+ def awards_supported?
+ noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)
+ end
+
+ def contains_emoji_only?
+ note =~ /\A#{Gitlab::Markdown::EmojiFilter.emoji_pattern}\s?\Z/
+ end
+
+ def award_emoji_name
+ note.match(Gitlab::Markdown::EmojiFilter.emoji_pattern)[1]
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index e912c48467d..af034a6692b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -37,12 +37,13 @@ class Project < ActiveRecord::Base
include Gitlab::ConfigHelper
include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
+ include Gitlab::CurrentSettings
include Referable
include Sortable
include AfterCommitQueue
+ include CaseSensitivity
extend Gitlab::ConfigHelper
- extend Enumerize
UNKNOWN_IMPORT_URL = 'http://unknown.git'
@@ -50,6 +51,7 @@ class Project < ActiveRecord::Base
default_value_for :visibility_level, gitlab_config_features.visibility_level
default_value_for :issues_enabled, gitlab_config_features.issues
default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
+ default_value_for :builds_enabled, gitlab_config_features.builds
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :wall_enabled, false
default_value_for :snippets_enabled, gitlab_config_features.snippets
@@ -118,6 +120,11 @@ class Project < ActiveRecord::Base
has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy
has_many :starrers, through: :users_star_projects, source: :user
+ has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id
+ has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build'
+ has_many :releases, dependent: :destroy
+ has_many :lfs_objects_projects, dependent: :destroy
+ has_many :lfs_objects, through: :lfs_objects_projects
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id
@@ -145,7 +152,7 @@ class Project < ActiveRecord::Base
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
validates :import_url,
- format: { with: /\A#{URI.regexp(%w(ssh git http https))}\z/, message: 'should be a valid url' },
+ url: { protocols: %w(ssh git http https) },
if: :external_import?
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
@@ -233,13 +240,19 @@ class Project < ActiveRecord::Base
end
def find_with_namespace(id)
- return nil unless id.include?('/')
+ namespace_path, project_path = id.split('/')
- id = id.split('/')
- namespace = Namespace.find_by(path: id.first)
- return nil unless namespace
+ return nil if !namespace_path || !project_path
- where(namespace_id: namespace.id).find_by(path: id.second)
+ # Use of unscoped ensures we're not secretly adding any ORDER BYs, which
+ # have a negative impact on performance (and aren't needed for this
+ # query).
+ projects = unscoped.
+ joins(:namespace).
+ iwhere('namespaces.path' => namespace_path)
+
+ projects.where('projects.path' => project_path).take ||
+ projects.iwhere('projects.path' => project_path).take
end
def visibility_levels
@@ -258,6 +271,24 @@ class Project < ActiveRecord::Base
name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR
%r{(?<project>#{name_pattern}/#{name_pattern})}
end
+
+ def trending(since = 1.month.ago)
+ # By counting in the JOIN we don't expose the GROUP BY to the outer query.
+ # This means that calls such as "any?" and "count" just return a number of
+ # the total count, instead of the counts grouped per project as a Hash.
+ join_body = "INNER JOIN (
+ SELECT project_id, COUNT(*) AS amount
+ FROM notes
+ WHERE created_at >= #{sanitize(since)}
+ GROUP BY project_id
+ ) join_note_counts ON projects.id = join_note_counts.project_id"
+
+ joins(join_body).reorder('join_note_counts.amount DESC')
+ end
+
+ def visible_to_user(user)
+ where(id: user.authorized_projects.select(:id).reorder(nil))
+ end
end
def team
@@ -282,15 +313,17 @@ class Project < ActiveRecord::Base
def add_import_job
if forked?
- unless RepositoryForkWorker.perform_async(id, forked_from_project.path_with_namespace, self.namespace.path)
- import_fail
- end
+ RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path)
else
- RepositoryImportWorker.perform_async(id)
+ RepositoryImportWorker.perform_async(self.id)
end
end
def clear_import_data
+ update(import_error: nil)
+
+ ProjectCacheWorker.perform_async(self.id)
+
self.import_data.destroy if self.import_data
end
@@ -318,6 +351,14 @@ class Project < ActiveRecord::Base
import_status == 'finished'
end
+ def safe_import_url
+ result = URI.parse(self.import_url)
+ result.password = '*****' unless result.password.nil?
+ result.to_s
+ rescue
+ original_url
+ end
+
def check_limit
unless creator.can_create_project? or namespace.kind == 'group'
errors[:limit_reached] << ("Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it")
@@ -411,7 +452,7 @@ class Project < ActiveRecord::Base
if template.nil?
# If no template, we should create an instance. Ex `create_gitlab_ci_service`
- service = self.send :"create_#{service_name}_service"
+ self.send :"create_#{service_name}_service"
else
Service.create_from_template(self.id, template)
end
@@ -432,10 +473,6 @@ class Project < ActiveRecord::Base
list.find { |service| service.to_param == name }
end
- def gitlab_ci?
- gitlab_ci_service && gitlab_ci_service.active && gitlab_ci_project.present?
- end
-
def ci_services
services.select { |service| service.category == :ci }
end
@@ -479,8 +516,8 @@ class Project < ActiveRecord::Base
end
end
- def send_move_instructions
- NotificationService.new.project_was_moved(self)
+ def send_move_instructions(old_path_with_namespace)
+ NotificationService.new.project_was_moved(self, old_path_with_namespace)
end
def owner
@@ -545,7 +582,7 @@ class Project < ActiveRecord::Base
end
def empty_repo?
- !repository.exists? || repository.empty?
+ !repository.exists? || !repository.has_visible_content?
end
def repo
@@ -622,7 +659,7 @@ class Project < ActiveRecord::Base
# So we basically we mute exceptions in next actions
begin
gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
- send_move_instructions
+ send_move_instructions(old_path_with_namespace)
reset_events_cache
rescue
# Returning false does not rollback after_* transaction but gives
@@ -634,6 +671,8 @@ class Project < ActiveRecord::Base
# db changes in order to prevent out of sync between db and fs
raise Exception.new('repository cannot be renamed')
end
+
+ Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path)
end
def hook_attrs
@@ -736,22 +775,52 @@ class Project < ActiveRecord::Base
def create_wiki
ProjectWiki.new(self, self.owner).wiki
true
- rescue ProjectWiki::CouldNotCreateWikiError => ex
+ rescue ProjectWiki::CouldNotCreateWikiError
errors.add(:base, 'Failed create wiki')
false
end
def ci_commit(sha)
- gitlab_ci_project.commits.find_by(sha: sha) if gitlab_ci?
+ ci_commits.find_by(sha: sha)
end
- def enable_ci(user)
- # Enable service
+ def ensure_ci_commit(sha)
+ ci_commit(sha) || ci_commits.create(sha: sha)
+ end
+
+ def ensure_gitlab_ci_project
+ gitlab_ci_project || create_gitlab_ci_project(
+ shared_runners_enabled: current_application_settings.shared_runners_enabled
+ )
+ end
+
+ # TODO: this should be migrated to Project table,
+ # the same as issues_enabled
+ def builds_enabled
+ gitlab_ci_service && gitlab_ci_service.active
+ end
+
+ def builds_enabled?
+ builds_enabled
+ end
+
+ def builds_enabled=(value)
service = gitlab_ci_service || create_gitlab_ci_service
- service.active = true
+ service.active = value
service.save
+ end
+
+ def enable_ci
+ self.builds_enabled = true
+ end
- # Create Ci::Project
- Ci::CreateProjectService.new.execute(user, self)
+ def unlink_fork
+ if forked?
+ forked_from_project.lfs_objects.find_each do |lfs_object|
+ lfs_object.projects << self
+ end
+
+ forked_project_link.destroy
+ end
end
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index d8aedbd2ab4..0a61ad96a0e 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -23,10 +23,7 @@ class BambooService < CiService
prop_accessor :bamboo_url, :build_key, :username, :password
- validates :bamboo_url,
- presence: true,
- format: { with: /\A#{URI.regexp}\z/ },
- if: :activated?
+ validates :bamboo_url, presence: true, url: true, if: :activated?
validates :build_key, presence: true, if: :activated?
validates :username,
presence: true,
@@ -40,12 +37,19 @@ class BambooService < CiService
attr_accessor :response
after_save :compose_service_hook, if: :activated?
+ before_update :reset_password
def compose_service_hook
hook = service_hook || build_service_hook
hook.save
end
+ def reset_password
+ if bamboo_url_changed? && !password_touched?
+ self.password = nil
+ end
+ end
+
def title
'Atlassian Bamboo CI'
end
@@ -77,7 +81,7 @@ class BambooService < CiService
def supported_events
%w(push)
end
-
+
def build_info(sha)
url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}")
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 40058b53df5..199ee3a9d0d 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -37,7 +37,7 @@ class BuildkiteService < CiService
def compose_service_hook
hook = service_hook || build_service_hook
hook.url = webhook_url
- hook.enable_ssl_verification = enable_ssl_verification
+ hook.enable_ssl_verification = !!enable_ssl_verification
hook.save
end
diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb
index 25c72033eac..d89466b689f 100644
--- a/app/models/project_services/ci/hip_chat_message.rb
+++ b/app/models/project_services/ci/hip_chat_message.rb
@@ -11,14 +11,7 @@ module Ci
def to_s
lines = Array.new
lines.push("<a href=\"#{ci_project_url(project)}\">#{project.name}</a> - ")
-
- if commit.matrix?
- lines.push("<a href=\"#{ci_project_ref_commits_url(project, commit.ref, commit.sha)}\">Commit ##{commit.id}</a></br>")
- else
- first_build = commit.builds_without_retry.first
- lines.push("<a href=\"#{ci_project_build_url(project, first_build)}\">Build '#{first_build.name}' ##{first_build.id}</a></br>")
- end
-
+ lines.push("<a href=\"#{builds_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}\">Commit ##{commit.id}</a></br>")
lines.push("#{commit.short_sha} #{commit.git_author_name} - #{commit.git_commit_message}</br>")
lines.push("#{humanized_status(commit_status)} in #{commit.duration} second(s).")
lines.join('')
diff --git a/app/models/project_services/ci/hip_chat_service.rb b/app/models/project_services/ci/hip_chat_service.rb
index 0e6e97394bc..0df03890efb 100644
--- a/app/models/project_services/ci/hip_chat_service.rb
+++ b/app/models/project_services/ci/hip_chat_service.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: services
+# Table name: ci_services
#
# id :integer not null, primary key
# type :string(255)
@@ -49,7 +49,7 @@ module Ci
commit = build.commit
return unless commit
- return unless commit.builds_without_retry.include? build
+ return unless commit.latest_builds.include? build
case commit.status.to_sym
when :failed
diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb
index 1bd2f33612b..bb961d06972 100644
--- a/app/models/project_services/ci/mail_service.rb
+++ b/app/models/project_services/ci/mail_service.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: services
+# Table name: ci_services
#
# id :integer not null, primary key
# type :string(255)
@@ -48,7 +48,7 @@ module Ci
# it doesn't make sense to send emails for retried builds
commit = build.commit
return unless commit
- return unless commit.builds_without_retry.include?(build)
+ return unless commit.latest_builds.include?(build)
case build.status.to_sym
when :failed
@@ -61,12 +61,12 @@ module Ci
end
def execute(build)
- build.commit.project_recipients.each do |recipient|
+ build.project_recipients.each do |recipient|
case build.status.to_sym
when :success
- mailer.build_success_email(build.id, recipient)
+ mailer.build_success_email(build.id, recipient).deliver_later
when :failed
- mailer.build_fail_email(build.id, recipient)
+ mailer.build_fail_email(build.id, recipient).deliver_later
end
end
end
@@ -78,7 +78,7 @@ module Ci
end
def mailer
- Ci::Notify.delay
+ Ci::Notify
end
end
end
diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb
index 757b1961143..1a6ff8e34c9 100644
--- a/app/models/project_services/ci/slack_message.rb
+++ b/app/models/project_services/ci/slack_message.rb
@@ -23,15 +23,13 @@ module Ci
def attachments
fields = []
- if commit.matrix?
- commit.builds_without_retry.each do |build|
- next if build.allow_failure?
- next unless build.failed?
- fields << {
- title: build.name,
- value: "Build <#{ci_project_build_url(project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)."
- }
- end
+ commit.latest_builds.each do |build|
+ next if build.allow_failure?
+ next unless build.failed?
+ fields << {
+ title: build.name,
+ value: "Build <#{namespace_project_build_url(build.gl_project.namespace, build.gl_project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)."
+ }
end
[{
@@ -47,12 +45,7 @@ module Ci
def attachment_message
out = "<#{ci_project_url(project)}|#{project_name}>: "
- if commit.matrix?
- out << "Commit <#{ci_project_ref_commits_url(project, commit.ref, commit.sha)}|\##{commit.id}> "
- else
- build = commit.builds_without_retry.first
- out << "Build <#{ci_project_build_url(project, build)}|\##{build.id}> "
- end
+ out << "Commit <#{builds_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}|\##{commit.id}> "
out << "(<#{commit_sha_link}|#{commit.short_sha}>) "
out << "of <#{commit_ref_link}|#{commit.ref}> "
out << "by #{commit.git_author_name} " if commit.git_author_name
diff --git a/app/models/project_services/ci/slack_service.rb b/app/models/project_services/ci/slack_service.rb
index 76db573dc17..7064bfe78db 100644
--- a/app/models/project_services/ci/slack_service.rb
+++ b/app/models/project_services/ci/slack_service.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: services
+# Table name: ci_services
#
# id :integer not null, primary key
# type :string(255)
@@ -48,7 +48,7 @@ module Ci
commit = build.commit
return unless commit
- return unless commit.builds_without_retry.include?(build)
+ return unless commit.latest_builds.include?(build)
case commit.status.to_sym
when :failed
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index c73c4b058a1..08e5ccb3855 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -19,21 +19,19 @@
#
class DroneCiService < CiService
-
+
prop_accessor :drone_url, :token, :enable_ssl_verification
- validates :drone_url,
- presence: true,
- format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated?
- validates :token,
- presence: true,
- if: :activated?
+
+ validates :drone_url, presence: true, url: true, if: :activated?
+ validates :token, presence: true, if: :activated?
after_save :compose_service_hook, if: :activated?
def compose_service_hook
hook = service_hook || build_service_hook
- hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join
- hook.enable_ssl_verification = enable_ssl_verification
+ # If using a service template, project may not be available
+ hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join if project
+ hook.enable_ssl_verification = !!enable_ssl_verification
hook.save
end
@@ -57,16 +55,16 @@ class DroneCiService < CiService
end
def merge_request_status_path(iid, sha = nil, ref = nil)
- url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}",
+ url = [drone_url,
+ "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}",
"?access_token=#{token}"]
URI.join(*url).to_s
end
def commit_status_path(sha, ref)
- url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}",
+ url = [drone_url,
+ "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}",
"?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"]
URI.join(*url).to_s
@@ -113,15 +111,15 @@ class DroneCiService < CiService
end
def merge_request_page(iid, sha, ref)
- url = [drone_url,
+ url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"]
URI.join(*url).to_s
end
def commit_page(sha, ref)
- url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}",
+ url = [drone_url,
+ "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}",
"?branch=#{URI::encode(ref.to_s)}"]
URI.join(*url).to_s
@@ -162,10 +160,10 @@ class DroneCiService < CiService
end
def push_valid?(data)
- opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id,
+ opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id,
source_branch: Gitlab::Git.ref_name(data[:ref]))
- opened_merge_requests.empty? && data[:total_commits_count] > 0 &&
+ opened_merge_requests.empty? && data[:total_commits_count] > 0 &&
!Gitlab::Git.blank_ref?(data[:after])
end
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index 9c46af7e721..74c57949b4d 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -22,10 +22,8 @@ class ExternalWikiService < Service
include HTTParty
prop_accessor :external_wiki_url
- validates :external_wiki_url,
- presence: true,
- format: { with: /\A#{URI.regexp}\z/ },
- if: :activated?
+
+ validates :external_wiki_url, presence: true, url: true, if: :activated?
def title
'External Wiki'
diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb
index 23ab206efba..234e8e8b580 100644
--- a/app/models/project_services/gitlab_ci_service.rb
+++ b/app/models/project_services/gitlab_ci_service.rb
@@ -22,12 +22,18 @@ class GitlabCiService < CiService
include Gitlab::Application.routes.url_helpers
after_save :compose_service_hook, if: :activated?
+ after_save :ensure_gitlab_ci_project, if: :activated?
def compose_service_hook
hook = service_hook || build_service_hook
hook.save
end
+ def ensure_gitlab_ci_project
+ return unless project
+ project.ensure_gitlab_ci_project
+ end
+
def supported_events
%w(push tag_push)
end
@@ -35,19 +41,10 @@ class GitlabCiService < CiService
def execute(data)
return unless supported_events.include?(data[:object_kind])
- sha = data[:checkout_sha]
-
- if sha.present?
- file = ci_yaml_file(sha)
-
- if file && file.data
- data.merge!(ci_yaml_file: file.data)
- end
- end
-
- ci_project = Ci::Project.find_by(gitlab_id: project.id)
+ ci_project = project.gitlab_ci_project
if ci_project
- Ci::CreateCommitService.new.execute(ci_project, data)
+ current_user = User.find_by(id: data[:user_id])
+ Ci::CreateCommitService.new.execute(ci_project, current_user, data)
end
end
@@ -58,7 +55,7 @@ class GitlabCiService < CiService
end
def get_ci_commit(sha, ref)
- Ci::Project.find(project.gitlab_ci_project).commits.find_by_sha_and_ref!(sha, ref)
+ Ci::Project.find(project.gitlab_ci_project.id).commits.find_by_sha!(sha)
end
def commit_status(sha, ref)
@@ -67,25 +64,6 @@ class GitlabCiService < CiService
:error
end
- def fork_registration(new_project, current_user)
- params = OpenStruct.new({
- id: new_project.id,
- name_with_namespace: new_project.name_with_namespace,
- path_with_namespace: new_project.path_with_namespace,
- web_url: new_project.web_url,
- default_branch: new_project.default_branch,
- ssh_url_to_repo: new_project.ssh_url_to_repo
- })
-
- ci_project = Ci::Project.find_by!(gitlab_id: project.id)
-
- Ci::CreateProjectService.new.execute(
- current_user,
- params,
- ci_project
- )
- end
-
def commit_coverage(sha, ref)
get_ci_commit(sha, ref).coverage
rescue ActiveRecord::RecordNotFound
@@ -94,7 +72,7 @@ class GitlabCiService < CiService
def build_page(sha, ref)
if project.gitlab_ci_project.present?
- ci_project_ref_commits_url(project.gitlab_ci_project, ref, sha)
+ builds_namespace_project_commit_url(project.namespace, project, sha)
end
end
@@ -113,14 +91,4 @@ class GitlabCiService < CiService
def fields
[]
end
-
- private
-
- def ci_yaml_file(sha)
- repository.blob_at(sha, '.gitlab-ci.yml')
- end
-
- def repository
- project.repository
- end
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 7a15a861abc..af2840a57f0 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -85,17 +85,16 @@ class HipchatService < Service
def create_message(data)
object_kind = data[:object_kind]
- message = \
- case object_kind
- when "push", "tag_push"
- create_push_message(data)
- when "issue"
- create_issue_message(data) unless is_update?(data)
- when "merge_request"
- create_merge_request_message(data) unless is_update?(data)
- when "note"
- create_note_message(data)
- end
+ case object_kind
+ when "push", "tag_push"
+ create_push_message(data)
+ when "issue"
+ create_issue_message(data) unless is_update?(data)
+ when "merge_request"
+ create_merge_request_message(data) unless is_update?(data)
+ when "note"
+ create_note_message(data)
+ end
end
def create_push_message(push)
@@ -167,8 +166,6 @@ class HipchatService < Service
obj_attr = data[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
merge_request_id = obj_attr[:iid]
- source_branch = obj_attr[:source_branch]
- target_branch = obj_attr[:target_branch]
state = obj_attr[:state]
description = obj_attr[:description]
title = obj_attr[:title]
@@ -194,8 +191,6 @@ class HipchatService < Service
data = HashWithIndifferentAccess.new(data)
user_name = data[:user][:name]
- repo_attr = HashWithIndifferentAccess.new(data[:repository])
-
obj_attr = HashWithIndifferentAccess.new(data[:object_attributes])
note = obj_attr[:note]
note_url = obj_attr[:url]
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb
index 074478b292d..b15d9a14677 100644
--- a/app/models/project_services/slack_service/note_message.rb
+++ b/app/models/project_services/slack_service/note_message.rb
@@ -45,30 +45,27 @@ class SlackService
def create_commit_note(commit)
commit_sha = commit[:id]
commit_sha = Commit.truncate_sha(commit_sha)
- commit_link = "[commit #{commit_sha}](#{@note_url})"
- title = format_title(commit[:message])
- @message = "#{@user_name} commented on #{commit_link} in #{project_link}: *#{title}*"
+ commented_on_message(
+ "[commit #{commit_sha}](#{@note_url})",
+ format_title(commit[:message]))
end
def create_issue_note(issue)
- issue_iid = issue[:iid]
- note_link = "[issue ##{issue_iid}](#{@note_url})"
- title = format_title(issue[:title])
- @message = "#{@user_name} commented on #{note_link} in #{project_link}: *#{title}*"
+ commented_on_message(
+ "[issue ##{issue[:iid]}](#{@note_url})",
+ format_title(issue[:title]))
end
def create_merge_note(merge_request)
- merge_request_id = merge_request[:iid]
- merge_request_link = "[merge request ##{merge_request_id}](#{@note_url})"
- title = format_title(merge_request[:title])
- @message = "#{@user_name} commented on #{merge_request_link} in #{project_link}: *#{title}*"
+ commented_on_message(
+ "[merge request ##{merge_request[:iid]}](#{@note_url})",
+ format_title(merge_request[:title]))
end
def create_snippet_note(snippet)
- snippet_id = snippet[:id]
- snippet_link = "[snippet ##{snippet_id}](#{@note_url})"
- title = format_title(snippet[:title])
- @message = "#{@user_name} commented on #{snippet_link} in #{project_link}: *#{title}*"
+ commented_on_message(
+ "[snippet ##{snippet[:id]}](#{@note_url})",
+ format_title(snippet[:title]))
end
def description_message
@@ -78,5 +75,9 @@ class SlackService
def project_link
"[#{@project_name}](#{@project_url})"
end
+
+ def commented_on_message(target_link, title)
+ @message = "#{@user_name} commented on #{target_link} in #{project_link}: *#{title}*"
+ end
end
end
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index 3c002a1634b..29d4236745a 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -23,26 +23,33 @@ class TeamcityService < CiService
prop_accessor :teamcity_url, :build_type, :username, :password
- validates :teamcity_url,
- presence: true,
- format: { with: /\A#{URI.regexp}\z/ }, if: :activated?
+ validates :teamcity_url, presence: true, url: true, if: :activated?
validates :build_type, presence: true, if: :activated?
validates :username,
presence: true,
- if: ->(service) { service.password? }, if: :activated?
+ if: ->(service) { service.password? },
+ if: :activated?
validates :password,
presence: true,
- if: ->(service) { service.username? }, if: :activated?
+ if: ->(service) { service.username? },
+ if: :activated?
attr_accessor :response
after_save :compose_service_hook, if: :activated?
+ before_update :reset_password
def compose_service_hook
hook = service_hook || build_service_hook
hook.save
end
+ def reset_password
+ if teamcity_url_changed? && !password_touched?
+ self.password = nil
+ end
+ end
+
def title
'JetBrains TeamCity CI'
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 56e49af2324..9f380a382cb 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -135,15 +135,32 @@ class ProjectTeam
!!find_member(user_id)
end
+ def human_max_access(user_id)
+ Gitlab::Access.options.key max_member_access(user_id)
+ end
+
+ # This method assumes project and group members are eager loaded for optimal
+ # performance.
def max_member_access(user_id)
access = []
- access << project.project_members.find_by(user_id: user_id).try(:access_field)
+
+ project.project_members.each do |member|
+ if member.user_id == user_id
+ access << member.access_field if member.access_field
+ break
+ end
+ end
if group
- access << group.group_members.find_by(user_id: user_id).try(:access_field)
+ group.group_members.each do |member|
+ if member.user_id == user_id
+ access << member.access_field if member.access_field
+ break
+ end
+ end
end
- access.compact.max
+ access.max
end
private
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 231973fa543..b5fec38378b 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -86,6 +86,8 @@ class ProjectWiki
commit = commit_details(:created, message, title)
wiki.write_page(title, format, content, commit)
+
+ update_project_activity
rescue Gollum::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}"
return false
@@ -95,10 +97,14 @@ class ProjectWiki
commit = commit_details(:updated, message, page.title)
wiki.update_page(page, page.name, format, content, commit)
+
+ update_project_activity
end
def delete_page(page, message = nil)
wiki.delete_page(page, commit_details(:deleted, message, page.title))
+
+ update_project_activity
end
def page_title_and_dir(title)
@@ -146,4 +152,8 @@ class ProjectWiki
def path_to_repo
@path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git")
end
+
+ def update_project_activity
+ @project.touch(:last_activity_at)
+ end
end
diff --git a/app/models/release.rb b/app/models/release.rb
new file mode 100644
index 00000000000..89f70278af5
--- /dev/null
+++ b/app/models/release.rb
@@ -0,0 +1,17 @@
+# == Schema Information
+#
+# Table name: releases
+#
+# id :integer not null, primary key
+# tag :string(255)
+# description :text
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+#
+
+class Release < ActiveRecord::Base
+ belongs_to :project
+
+ validates :description, :project, :tag, presence: true
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 79b48ebfedf..1d43307e1e7 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1,24 +1,35 @@
require 'securerandom'
class Repository
- class PreReceiveError < StandardError; end
class CommitError < StandardError; end
include Gitlab::ShellAdapter
- attr_accessor :raw_repository, :path_with_namespace, :project
+ attr_accessor :path_with_namespace, :project
+
+ def self.clean_old_archives
+ repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
+
+ return unless File.directory?(repository_downloads_path)
+
+ 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)
@path_with_namespace = path_with_namespace
@project = project
+ end
- if path_with_namespace
- @raw_repository = Gitlab::Git::Repository.new(path_to_repo)
- @raw_repository.autocrlf = :input
- end
+ def raw_repository
+ return nil unless path_with_namespace
- rescue Gitlab::Git::Repository::NoRepository
- nil
+ @raw_repository ||= begin
+ repo = Gitlab::Git::Repository.new(path_to_repo)
+ repo.autocrlf = :input
+ repo
+ rescue Gitlab::Git::Repository::NoRepository
+ nil
+ end
end
# Return absolute path to repository
@@ -36,6 +47,19 @@ class Repository
raw_repository.empty?
end
+ #
+ # Git repository can contains some hidden refs like:
+ # /refs/notes/*
+ # /refs/git-as-svn/*
+ # /refs/pulls/*
+ # This refs by default not visible in project page and not cloned to client side.
+ #
+ # This method return true if repository contains some content visible in project page.
+ #
+ def has_visible_content?
+ !raw_repository.branches.empty?
+ end
+
def commit(id = 'HEAD')
return nil unless raw_repository
commit = Gitlab::Git::Commit.find(raw_repository, id)
@@ -46,13 +70,16 @@ class Repository
end
def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false)
- commits = Gitlab::Git::Commit.where(
+ options = {
repo: raw_repository,
ref: ref,
path: path,
limit: limit,
offset: offset,
- )
+ follow: path.present?
+ }
+
+ commits = Gitlab::Git::Commit.where(options)
commits = Commit.decorate(commits, @project) if commits.present?
commits
end
@@ -63,38 +90,62 @@ class Repository
commits
end
+ def find_commits_by_message(query)
+ # Limited to 1000 commits for now, could be parameterized?
+ args = %W(#{Gitlab.config.git.bin_path} log --pretty=%H --max-count 1000 --grep=#{query})
+
+ git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp)
+ commits = git_log_results.map { |c| commit(c) }
+ commits
+ end
+
def find_branch(name)
- branches.find { |branch| branch.name == name }
+ raw_repository.branches.find { |branch| branch.name == name }
end
def find_tag(name)
- tags.find { |tag| tag.name == name }
+ raw_repository.tags.find { |tag| tag.name == name }
end
- def add_branch(branch_name, ref)
- cache.expire(:branch_names)
- @branches = nil
+ def add_branch(user, branch_name, target)
+ oldrev = Gitlab::Git::BLANK_SHA
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
+ target = commit(target).try(:id)
+
+ return false unless target
- gitlab_shell.add_branch(path_with_namespace, branch_name, ref)
+ GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
+ rugged.branches.create(branch_name, target)
+ end
+
+ expire_branches_cache
+ find_branch(branch_name)
end
def add_tag(tag_name, ref, message = nil)
- cache.expire(:tag_names)
- @tags = nil
+ expire_tags_cache
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end
- def rm_branch(branch_name)
- cache.expire(:branch_names)
- @branches = nil
+ def rm_branch(user, branch_name)
+ expire_branches_cache
+
+ branch = find_branch(branch_name)
+ oldrev = branch.try(:target)
+ newrev = Gitlab::Git::BLANK_SHA
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
+
+ GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
+ rugged.branches.delete(branch_name)
+ end
- gitlab_shell.rm_branch(path_with_namespace, branch_name)
+ expire_branches_cache
+ true
end
def rm_tag(tag_name)
- cache.expire(:tag_names)
- @tags = nil
+ expire_tags_cache
gitlab_shell.rm_tag(path_with_namespace, tag_name)
end
@@ -136,6 +187,16 @@ class Repository
end
end
+ def expire_tags_cache
+ cache.expire(:tag_names)
+ @tags = nil
+ end
+
+ def expire_branches_cache
+ cache.expire(:branch_names)
+ @branches = nil
+ end
+
def expire_cache
cache_keys.each do |key|
cache.expire(key)
@@ -263,20 +324,12 @@ class Repository
end
def last_commit_for_path(sha, path)
- args = %W(git rev-list --max-count=1 #{sha} -- #{path})
+ args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
sha = Gitlab::Popen.popen(args, path_to_repo).first.strip
commit(sha)
end
# Remove archives older than 2 hours
- def clean_old_archives
- repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
-
- return unless File.directory?(repository_downloads_path)
-
- Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
- end
-
def branches_sorted_by(value)
case value
when 'recently_updated'
@@ -312,13 +365,7 @@ class Repository
end
def blob_for_diff(commit, diff)
- file = blob_at(commit.id, diff.new_path)
-
- unless file
- file = prev_blob_for_diff(commit, diff)
- end
-
- file
+ blob_at(commit.id, diff.file_path)
end
def prev_blob_for_diff(commit, diff)
@@ -327,8 +374,8 @@ class Repository
end
end
- def branch_names_contains(sha)
- args = %W(git branch --contains #{sha})
+ def refs_contains_sha(ref_type, sha)
+ args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
names = Gitlab::Popen.popen(args, path_to_repo).first
if names.respond_to?(:split)
@@ -344,21 +391,12 @@ class Repository
end
end
- def tag_names_contains(sha)
- args = %W(git tag --contains #{sha})
- names = Gitlab::Popen.popen(args, path_to_repo).first
-
- if names.respond_to?(:split)
- names = names.split("\n").map(&:strip)
-
- names.each do |name|
- name.slice! '* '
- end
+ def branch_names_contains(sha)
+ refs_contains_sha('branch', sha)
+ end
- names
- else
- []
- end
+ def tag_names_contains(sha)
+ refs_contains_sha('tag', sha)
end
def branches
@@ -373,11 +411,25 @@ class Repository
@root_ref ||= raw_repository.root_ref
end
- def commit_file(user, path, content, message, branch)
+ def commit_dir(user, path, message, branch)
commit_with_hooks(user, branch) do |ref|
- path[0] = '' if path[0] == '/'
+ committer = user_to_committer(user)
+ options = {}
+ options[:committer] = committer
+ options[:author] = committer
+
+ options[:commit] = {
+ message: message,
+ branch: ref,
+ }
+
+ raw_repository.mkdir(path, options)
+ end
+ end
- committer = user_to_comitter(user)
+ def commit_file(user, path, content, message, branch, update)
+ commit_with_hooks(user, branch) do |ref|
+ committer = user_to_committer(user)
options = {}
options[:committer] = committer
options[:author] = committer
@@ -388,7 +440,8 @@ class Repository
options[:file] = {
content: content,
- path: path
+ path: path,
+ update: update
}
Gitlab::Git::Blob.commit(raw_repository, options)
@@ -397,9 +450,7 @@ class Repository
def remove_file(user, path, message, branch)
commit_with_hooks(user, branch) do |ref|
- path[0] = '' if path[0] == '/'
-
- committer = user_to_comitter(user)
+ committer = user_to_committer(user)
options = {}
options[:committer] = committer
options[:author] = committer
@@ -416,7 +467,7 @@ class Repository
end
end
- def user_to_comitter(user)
+ def user_to_committer(user)
{
email: user.email,
name: user.name,
@@ -461,15 +512,24 @@ class Repository
root_ref_commit = commit(root_ref)
if branch_commit
- rugged.merge_base(root_ref_commit.id, branch_commit.id) == branch_commit.id
+ is_ancestor?(branch_commit.id, root_ref_commit.id)
else
nil
end
end
+ def merge_base(first_commit_id, second_commit_id)
+ rugged.merge_base(first_commit_id, second_commit_id)
+ end
+
+ def is_ancestor?(ancestor_id, descendant_id)
+ merge_base(ancestor_id, descendant_id) == ancestor_id
+ end
+
+
def search_files(query, ref)
offset = 2
- args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} #{query} #{ref || root_ref})
+ args = %W(#{Gitlab.config.git.bin_path} grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
@@ -501,14 +561,13 @@ class Repository
end
def fetch_ref(source_path, source_ref, target_ref)
- args = %W(git fetch #{source_path} #{source_ref}:#{target_ref})
+ args = %W(#{Gitlab.config.git.bin_path} fetch -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
end
def commit_with_hooks(current_user, branch)
oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
- gl_id = Gitlab::ShellEnv.gl_id(current_user)
was_empty = empty?
# Create temporary ref
@@ -527,11 +586,7 @@ class Repository
raise CommitError.new('Failed to create commit')
end
- # Run GitLab pre-receive hook
- pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', path_to_repo)
- status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref)
-
- if status
+ GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
if was_empty
# Create branch
rugged.references.create(ref, newrev)
@@ -546,16 +601,11 @@ class Repository
raise CommitError.new('Commit was rejected because branch received new push')
end
end
-
- # Run GitLab post receive hook
- post_receive_hook = Gitlab::Git::Hook.new('post-receive', path_to_repo)
- status = post_receive_hook.trigger(gl_id, oldrev, newrev, ref)
- else
- # Remove tmp ref and return error to user
- rugged.references.delete(tmp_ref)
-
- raise PreReceiveError.new('Commit was rejected by pre-receive hook')
end
+ rescue GitHooksService::PreReceiveError
+ # Remove tmp ref and return error to user
+ rugged.references.delete(tmp_ref)
+ raise
end
private
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 3eed5c16e45..f36eda1531b 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -17,12 +17,11 @@ class SentNotification < ActiveRecord::Base
belongs_to :noteable, polymorphic: true
belongs_to :recipient, class_name: "User"
- validate :project, :recipient, :reply_key, presence: true
- validate :reply_key, uniqueness: true
-
+ validates :project, :recipient, :reply_key, presence: true
+ validates :reply_key, uniqueness: true
validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
- validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
+ validates :line_code, line_code: true, allow_blank: true
class << self
def reply_key
diff --git a/app/models/service.rb b/app/models/service.rb
index 60fcc9d2857..d610abd1683 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -33,6 +33,8 @@ class Service < ActiveRecord::Base
after_initialize :initialize_properties
+ after_commit :reset_updated_properties
+
belongs_to :project
has_one :service_hook
@@ -103,6 +105,7 @@ class Service < ActiveRecord::Base
# Provide convenient accessor methods
# for each serialized property.
+ # Also keep track of updated properties in a similar way as ActiveModel::Dirty
def self.prop_accessor(*args)
args.each do |arg|
class_eval %{
@@ -111,12 +114,39 @@ class Service < ActiveRecord::Base
end
def #{arg}=(value)
+ updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
self.properties['#{arg}'] = value
end
+
+ def #{arg}_changed?
+ #{arg}_touched? && #{arg} != #{arg}_was
+ end
+
+ def #{arg}_touched?
+ updated_properties.include?('#{arg}')
+ end
+
+ def #{arg}_was
+ updated_properties['#{arg}']
+ end
}
end
end
+ # Returns a hash of the properties that have been assigned a new value since last save,
+ # indicating their original values (attr => original value).
+ # ActiveRecord does not provide a mechanism to track changes in serialized keys,
+ # 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.
+ def updated_properties
+ @updated_properties ||= ActiveSupport::HashWithIndifferentAccess.new
+ end
+
+ 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 b0831982aa7..f876be7a4c8 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -65,6 +65,10 @@ class Snippet < ActiveRecord::Base
}x
end
+ def self.link_reference_pattern
+ super("snippets", /(?<snippet>\d+)/)
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{id}"
diff --git a/app/models/user.rb b/app/models/user.rb
index 25371f9138a..cfed797e725 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -54,6 +54,8 @@
# public_email :string(255) default(""), not null
# dashboard :integer default(0)
# project_view :integer default(0)
+# consumed_timestep :integer
+# layout :integer default(0)
#
require 'carrierwave/orm/activerecord'
@@ -67,6 +69,7 @@ class User < ActiveRecord::Base
include Referable
include Sortable
include TokenAuthenticatable
+ include CaseSensitivity
default_value_for :admin, false
default_value_for :can_create_group, gitlab_config.default_can_create_group
@@ -129,6 +132,8 @@ class User < ActiveRecord::Base
has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
+ has_one :abuse_report, dependent: :destroy
+ has_many :ci_builds, dependent: :nullify, class_name: 'Ci::Build'
#
@@ -143,11 +148,9 @@ class User < ActiveRecord::Base
validates :bio, length: { maximum: 255 }, allow_blank: true
validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :username,
+ namespace: true,
presence: true,
- uniqueness: { case_sensitive: false },
- exclusion: { in: Gitlab::Blacklist.path },
- format: { with: Gitlab::Regex.namespace_regex,
- message: Gitlab::Regex.namespace_regex_message }
+ uniqueness: { case_sensitive: false }
validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true
validate :namespace_uniq, if: ->(user) { user.username_changed? }
@@ -170,13 +173,16 @@ class User < ActiveRecord::Base
after_create :post_create_hook
after_destroy :post_destroy_hook
+ # User's Layout preference
+ enum layout: [:fixed, :fluid]
+
# User's Dashboard preference
# Note: When adding an option, it MUST go on the end of the array.
- enum dashboard: [:projects, :stars]
+ enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity]
# User's Project preference
# Note: When adding an option, it MUST go on the end of the array.
- enum project_view: [:readme, :activity]
+ enum project_view: [:readme, :activity, :files]
alias_attribute :private_token, :authentication_token
@@ -228,21 +234,16 @@ class User < ActiveRecord::Base
# Find a User by their primary email or any associated secondary email
def find_by_any_email(email)
- user_table = arel_table
- email_table = Email.arel_table
-
- # Use ARel to build a query:
- query = user_table.
- # SELECT "users".* FROM "users"
- project(user_table[Arel.star]).
- # LEFT OUTER JOIN "emails"
- join(email_table, Arel::Nodes::OuterJoin).
- # ON "users"."id" = "emails"."user_id"
- on(user_table[:id].eq(email_table[:user_id])).
- # WHERE ("user"."email" = '<email>' OR "emails"."email" = '<email>')
- where(user_table[:email].eq(email).or(email_table[:email].eq(email)))
-
- find_by_sql(query.to_sql).first
+ sql = 'SELECT *
+ FROM users
+ WHERE id IN (
+ SELECT id FROM users WHERE email = :email
+ UNION
+ SELECT emails.user_id FROM emails WHERE email = :email
+ )
+ LIMIT 1;'
+
+ User.find_by_sql([sql, { email: email }]).first
end
def filter(filter_name)
@@ -267,8 +268,13 @@ class User < ActiveRecord::Base
end
def by_login(login)
- where('lower(username) = :value OR lower(email) = :value',
- value: login.to_s.downcase).first
+ return nil unless login
+
+ if login.include?('@'.freeze)
+ unscoped.iwhere(email: login).take
+ else
+ unscoped.iwhere(username: login).take
+ end
end
def find_by_username!(username)
@@ -327,6 +333,10 @@ class User < ActiveRecord::Base
@reset_token
end
+ def recently_sent_password_reset?
+ reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
+ end
+
def disable_two_factor!
update_attributes(
two_factor_enabled: false,
@@ -377,31 +387,23 @@ class User < ActiveRecord::Base
end
end
- # Groups user has access to
+ # Returns the groups a user has access to
def authorized_groups
- @authorized_groups ||= begin
- group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id))
- Group.where(id: group_ids)
- end
- end
+ union = Gitlab::SQL::Union.
+ new([groups.select(:id), authorized_projects.select(:namespace_id)])
+ Group.where("namespaces.id IN (#{union.to_sql})")
+ end
- # Projects user has access to
+ # Returns the groups a user is authorized to access.
def authorized_projects
- @authorized_projects ||= begin
- project_ids = personal_projects.pluck(:id)
- project_ids.push(*groups_projects.pluck(:id))
- project_ids.push(*projects.pluck(:id).uniq)
- Project.where(id: project_ids)
- end
+ Project.where("projects.id IN (#{projects_union.to_sql})")
end
def owned_projects
@owned_projects ||=
- begin
- namespace_ids = owned_groups.pluck(:id).push(namespace.id)
- Project.in_namespace(namespace_ids).joins(:namespace)
- end
+ Project.where('namespace_id IN (?) OR namespace_id = ?',
+ owned_groups.select(:id), namespace.id).joins(:namespace)
end
# Team membership in authorized projects
@@ -633,11 +635,11 @@ class User < ActiveRecord::Base
email.start_with?('temp-email-for-oauth')
end
- def avatar_url(size = nil)
+ def avatar_url(size = nil, scale = 2)
if avatar.present?
[gitlab_config.url, avatar.url].join
else
- GravatarService.new.execute(email, size)
+ GravatarService.new.execute(email, size, scale)
end
end
@@ -686,27 +688,24 @@ class User < ActiveRecord::Base
end
def starred?(project)
- starred_projects.exists?(project)
+ starred_projects.exists?(project.id)
end
def toggle_star(project)
- user_star_project = users_star_projects.
- where(project: project, user: self).take
- if user_star_project
- user_star_project.destroy
- else
- UsersStarProject.create!(project: project, user: self)
+ UsersStarProject.transaction do
+ user_star_project = users_star_projects.
+ where(project: project, user: self).lock(true).first
+
+ if user_star_project
+ user_star_project.destroy
+ else
+ UsersStarProject.create!(project: project, user: self)
+ end
end
end
def manageable_namespaces
- @manageable_namespaces ||=
- begin
- namespaces = []
- namespaces << namespace
- namespaces += owned_groups
- namespaces += masters_groups
- end
+ @manageable_namespaces ||= [namespace] + owned_groups + masters_groups
end
def namespaces
@@ -719,12 +718,25 @@ class User < ActiveRecord::Base
Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
end
- def contributed_projects_ids
- Event.contributions.where(author_id: self).
+ # Returns the projects a user contributed to in the last year.
+ #
+ # This method relies on a subquery as this performs significantly better
+ # compared to a JOIN when coupled with, for example,
+ # `Project.visible_to_user`. That is, consider the following code:
+ #
+ # some_user.contributed_projects.visible_to_user(other_user)
+ #
+ # If this method were to use a JOIN the resulting query would take roughly 200
+ # ms on a database with a similar size to GitLab.com's database. On the other
+ # hand, using a subquery means we can get the exact same data in about 40 ms.
+ def contributed_projects
+ events = Event.select(:project_id).
+ contributions.where(author_id: self).
where("created_at > ?", Time.now - 1.year).
- reorder(project_id: :desc).
- select(:project_id).
- uniq.map(&:project_id)
+ uniq.
+ reorder(nil)
+
+ Project.where(id: events)
end
def restricted_signup_domains
@@ -754,12 +766,35 @@ class User < ActiveRecord::Base
!solo_owned_groups.present?
end
- def ci_authorized_projects
- @ci_authorized_projects ||= Ci::Project.where(gitlab_id: authorized_projects)
+ def ci_authorized_runners
+ @ci_authorized_runners ||= begin
+ runner_ids = Ci::RunnerProject.joins(:project).
+ where("ci_projects.gitlab_id IN (#{ci_projects_union.to_sql})").
+ select(:runner_id)
+
+ Ci::Runner.specific.where(id: runner_ids)
+ end
end
- def ci_authorized_runners
- Ci::Runner.specific.includes(:runner_projects).
- where(ci_runner_projects: { project_id: ci_authorized_projects } )
+ private
+
+ def projects_union
+ Gitlab::SQL::Union.new([personal_projects.select(:id),
+ groups_projects.select(:id),
+ projects.select(:id)])
+ end
+
+ def ci_projects_union
+ scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
+ groups = groups_projects.where(members: scope)
+ other = projects.where(members: scope)
+
+ Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id),
+ other.select(:id)])
+ end
+
+ # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
+ def send_devise_notification(notification, *args)
+ devise_mailer.send(notification, self, *args).deliver_later
end
end
diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb
index 3d49cb05949..413f3f485a8 100644
--- a/app/models/users_star_project.rb
+++ b/app/models/users_star_project.rb
@@ -10,7 +10,7 @@
#
class UsersStarProject < ActiveRecord::Base
- belongs_to :project, counter_cache: :star_count
+ belongs_to :project, counter_cache: :star_count, touch: true
belongs_to :user
validates :user, presence: true
diff --git a/app/services/archive_repository_service.rb b/app/services/archive_repository_service.rb
index e1b41527d8d..2160bf13e6d 100644
--- a/app/services/archive_repository_service.rb
+++ b/app/services/archive_repository_service.rb
@@ -7,19 +7,12 @@ class ArchiveRepositoryService
end
def execute(options = {})
- project.repository.clean_old_archives
+ RepositoryArchiveCacheWorker.perform_async
- raise "No archive file path" unless file_path
+ metadata = project.repository.archive_metadata(ref, storage_path, format)
+ raise "Repository or ref not found" if metadata.empty?
- return file_path if archived?
-
- unless archiving?
- RepositoryArchiveWorker.perform_async(project.id, ref, format)
- end
-
- archived = wait_until_archived(options[:timeout] || 5.0)
-
- file_path if archived
+ metadata
end
private
@@ -27,36 +20,4 @@ class ArchiveRepositoryService
def storage_path
Gitlab.config.gitlab.repository_downloads_path
end
-
- def file_path
- @file_path ||= project.repository.archive_file_path(ref, storage_path, format)
- end
-
- def pid_file_path
- @pid_file_path ||= project.repository.archive_pid_file_path(ref, storage_path, format)
- end
-
- def archived?
- File.exist?(file_path)
- end
-
- def archiving?
- File.exist?(pid_file_path)
- end
-
- def wait_until_archived(timeout = 5.0)
- return archived? if timeout == 0.0
-
- t1 = Time.now
-
- begin
- sleep 0.1
-
- success = archived?
-
- t2 = Time.now
- end until success || t2 - t1 >= timeout
-
- success
- end
end
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
new file mode 100644
index 00000000000..912eb6258a4
--- /dev/null
+++ b/app/services/ci/create_builds_service.rb
@@ -0,0 +1,39 @@
+module Ci
+ class CreateBuildsService
+ def execute(commit, stage, ref, tag, user, trigger_request, status)
+ builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag)
+
+ # check when to create next build
+ builds_attrs = builds_attrs.select do |build_attrs|
+ case build_attrs[:when]
+ when 'on_success'
+ status == 'success'
+ when 'on_failure'
+ status == 'failed'
+ when 'always'
+ %w(success failed).include?(status)
+ end
+ end
+
+ builds_attrs.map do |build_attrs|
+ # don't create the same build twice
+ unless commit.builds.find_by(ref: ref, tag: tag, trigger_request: trigger_request, name: build_attrs[:name])
+ build_attrs.slice!(:name,
+ :commands,
+ :tag_list,
+ :options,
+ :allow_failure,
+ :stage,
+ :stage_idx)
+
+ build_attrs.merge!(ref: ref,
+ tag: tag,
+ trigger_request: trigger_request,
+ user: user)
+
+ commit.builds.create!(build_attrs)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb
index 0a1abf89a95..479a2d6defc 100644
--- a/app/services/ci/create_commit_service.rb
+++ b/app/services/ci/create_commit_service.rb
@@ -1,7 +1,6 @@
module Ci
class CreateCommitService
- def execute(project, params)
- before_sha = params[:before]
+ def execute(project, user, params)
sha = params[:checkout_sha] || params[:after]
origin_ref = params[:ref]
@@ -16,34 +15,13 @@ module Ci
return false
end
- commit = project.commits.find_by_sha_and_ref(sha, ref)
-
- # Create commit if not exists yet
- unless commit
- data = {
- ref: ref,
- sha: sha,
- tag: origin_ref.start_with?('refs/tags/'),
- before_sha: before_sha,
- push_data: {
- before: before_sha,
- after: sha,
- ref: ref,
- user_name: params[:user_name],
- user_email: params[:user_email],
- repository: params[:repository],
- commits: params[:commits],
- total_commits_count: params[:total_commits_count],
- ci_yaml_file: params[:ci_yaml_file]
- }
- }
-
- commit = project.commits.create(data)
+ tag = origin_ref.start_with?('refs/tags/')
+ commit = project.gl_project.ensure_ci_commit(sha)
+ unless commit.skip_ci?
+ commit.update_committed!
+ commit.create_builds(ref, tag, user)
end
- commit.update_committed!
- commit.create_builds unless commit.builds.any?
-
commit
end
end
diff --git a/app/services/ci/create_project_service.rb b/app/services/ci/create_project_service.rb
deleted file mode 100644
index f42babd2388..00000000000
--- a/app/services/ci/create_project_service.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module Ci
- class CreateProjectService
- include Gitlab::Application.routes.url_helpers
-
- def execute(current_user, params, forked_project = nil)
- @project = Ci::Project.parse(params)
-
- Ci::Project.transaction do
- @project.save!
-
- gl_project = ::Project.find(@project.gitlab_id)
- gl_project.build_missing_services
- gl_project.gitlab_ci_service.update_attributes(active: true)
- end
-
- if forked_project
- # Copy settings
- settings = forked_project.attributes.select do |attr_name, value|
- ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name
- end
-
- @project.update(settings)
- end
-
- Ci::EventService.new.create_project(current_user, @project)
-
- @project
- end
- end
-end
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index 9bad09f2f54..4b86cb0a1f5 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -1,15 +1,20 @@
module Ci
class CreateTriggerRequestService
def execute(project, trigger, ref, variables = nil)
- commit = project.commits.where(ref: ref).last
+ commit = project.gl_project.commit(ref)
return unless commit
+ # check if ref is tag
+ tag = project.gl_project.repository.find_tag(ref).present?
+
+ ci_commit = project.gl_project.ensure_ci_commit(commit.sha)
+
trigger_request = trigger.trigger_requests.create!(
- commit: commit,
- variables: variables
+ variables: variables,
+ commit: ci_commit,
)
- if commit.create_builds(trigger_request)
+ if ci_commit.create_builds(ref, tag, nil, trigger_request)
trigger_request
end
end
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index b95835ba093..b8d24193035 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -1,17 +1,15 @@
module Ci
class ImageForBuildService
def execute(project, params)
- image_name =
- if params[:sha]
- commit = project.commits.find_by(sha: params[:sha])
- image_for_commit(commit)
- elsif params[:ref]
- commit = project.last_commit_for_ref(params[:ref])
- image_for_commit(commit)
- else
- 'build-unknown.svg'
+ sha = params[:sha]
+ sha ||=
+ if params[:ref]
+ project.gl_project.commit(params[:ref]).try(:sha)
end
+ commit = project.commits.ordered.find_by(sha: sha)
+ image_name = image_for_commit(commit)
+
image_path = Rails.root.join('public/ci', image_name)
OpenStruct.new(
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
index 33f1c1e918d..7beb098659c 100644
--- a/app/services/ci/register_build_service.rb
+++ b/app/services/ci/register_build_service.rb
@@ -8,18 +8,18 @@ module Ci
builds =
if current_runner.shared?
# don't run projects which have not enables shared runners
- builds.includes(:project).where(ci_projects: { shared_runners_enabled: true })
+ builds.joins(commit: { gl_project: :gitlab_ci_project }).where(ci_projects: { shared_runners_enabled: true })
else
# do run projects which are only assigned to this runner
- builds.where(project_id: current_runner.projects)
+ builds.joins(:commit).where(ci_commits: { gl_project_id: current_runner.gl_projects_ids })
end
builds = builds.order('created_at ASC')
build = builds.find do |build|
- (build.tag_list - current_runner.tag_list).empty?
+ build.can_be_served?(current_runner)
end
-
+
if build
# In case when 2 runners try to assign the same build, second runner will be declined
diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb
index 87984b20fa1..92e6df442b4 100644
--- a/app/services/ci/web_hook_service.rb
+++ b/app/services/ci/web_hook_service.rb
@@ -27,9 +27,8 @@ module Ci
project_name: project.name,
gitlab_url: project.gitlab_url,
ref: build.ref,
- sha: build.sha,
before_sha: build.before_sha,
- push_data: build.commit.push_data
+ sha: build.sha,
})
end
end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index bfe6a3dc4be..ec581658fc1 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -3,7 +3,7 @@ require 'securerandom'
# Compare 2 branches for one repo or between repositories
# and return Gitlab::CompareResult object that responds to commits and diffs
class CompareService
- def execute(source_project, source_branch, target_project, target_branch)
+ def execute(source_project, source_branch, target_project, target_branch, diff_options = {})
source_commit = source_project.commit(source_branch)
return unless source_commit
@@ -25,7 +25,7 @@ class CompareService
target_project.repository.raw_repository,
target_branch,
source_sha,
- )
+ ), diff_options
)
end
end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index cf7ae4345f3..de18f3bc556 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -13,8 +13,7 @@ class CreateBranchService < BaseService
return error('Branch already exists')
end
- repository.add_branch(branch_name, ref)
- new_branch = repository.find_branch(branch_name)
+ new_branch = repository.add_branch(current_user, branch_name, ref)
if new_branch
push_data = build_push_data(project, current_user, new_branch)
@@ -27,6 +26,8 @@ class CreateBranchService < BaseService
else
error('Invalid reference name')
end
+ rescue GitHooksService::PreReceiveError
+ error('Branch creation was rejected by Git hook')
end
def success(branch)
diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb
new file mode 100644
index 00000000000..e06a6f2f47a
--- /dev/null
+++ b/app/services/create_release_service.rb
@@ -0,0 +1,31 @@
+require_relative 'base_service'
+
+class CreateReleaseService < BaseService
+ def execute(tag_name, release_description)
+
+ repository = project.repository
+ existing_tag = repository.find_tag(tag_name)
+
+ # Only create a release if the tag exists
+ if existing_tag
+ release = project.releases.find_by(tag: tag_name)
+
+ if release
+ error('Release already exists', 409)
+ else
+ release = project.releases.new({ tag: tag_name, description: release_description })
+ release.save
+
+ success(release)
+ end
+ else
+ error('Tag does not exist', 404)
+ end
+ end
+
+ def success(release)
+ out = super()
+ out[:release] = release
+ out
+ end
+end
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
index 1a7318048b3..2452999382a 100644
--- a/app/services/create_tag_service.rb
+++ b/app/services/create_tag_service.rb
@@ -1,7 +1,7 @@
require_relative 'base_service'
class CreateTagService < BaseService
- def execute(tag_name, ref, message)
+ def execute(tag_name, ref, message, release_description = nil)
valid_tag = Gitlab::GitRefValidator.validate(tag_name)
if valid_tag == false
return error('Tag name invalid')
@@ -20,11 +20,15 @@ class CreateTagService < BaseService
if new_tag
push_data = create_push_data(project, current_user, new_tag)
-
EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :tag_push_hooks)
project.execute_services(push_data.dup, :tag_push_hooks)
+ if release_description
+ CreateReleaseService.new(@project, @current_user).
+ execute(tag_name, release_description)
+ end
+
success(new_tag)
else
error('Invalid reference name')
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index b19b112a0c4..22bf9dd935e 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -24,7 +24,7 @@ class DeleteBranchService < BaseService
return error('You dont have push access to repo', 405)
end
- if repository.rm_branch(branch_name)
+ if repository.rm_branch(current_user, branch_name)
push_data = build_push_data(branch)
EventCreateService.new.push(project, current_user, push_data)
@@ -35,6 +35,8 @@ class DeleteBranchService < BaseService
else
error('Failed to remove branch')
end
+ rescue GitHooksService::PreReceiveError
+ error('Branch deletion was rejected by Git hook')
end
def error(message, return_code = 400)
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index 0c836401136..de3352a6756 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -11,8 +11,10 @@ class DeleteTagService < BaseService
end
if repository.rm_tag(tag_name)
+ release = project.releases.find_by(tag: tag_name)
+ release.destroy if release
+
push_data = build_push_data(tag)
-
EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :tag_push_hooks)
project.execute_services(push_data.dup, :tag_push_hooks)
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 7aecee217d8..f50aaf2eb52 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -21,12 +21,12 @@ module Files
create_target_branch
end
- if sha = commit
+ if commit
success
else
error("Something went wrong. Your changes were not committed")
end
- rescue Repository::CommitError, Repository::PreReceiveError, ValidationError => ex
+ rescue Repository::CommitError, GitHooksService::PreReceiveError, ValidationError => ex
error(ex.message)
end
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
new file mode 100644
index 00000000000..6107254a34e
--- /dev/null
+++ b/app/services/files/create_dir_service.rb
@@ -0,0 +1,20 @@
+require_relative "base_service"
+
+module Files
+ class CreateDirService < Files::BaseService
+ def commit
+ repository.commit_dir(current_user, @file_path, @commit_message, @target_branch)
+ end
+
+ def validate
+ super
+
+ unless @file_path =~ Gitlab::Regex.file_path_regex
+ raise_error(
+ 'Your changes could not be committed, because the file path ' +
+ Gitlab::Regex.file_path_regex_message
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index ffbb5993279..2348920cc58 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -3,18 +3,23 @@ require_relative "base_service"
module Files
class CreateService < Files::BaseService
def commit
- repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch)
+ repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false)
end
def validate
super
- file_name = File.basename(@file_path)
+ if @file_path =~ Gitlab::Regex.directory_traversal_regex
+ raise_error(
+ 'Your changes could not be committed, because the file name ' +
+ Gitlab::Regex.directory_traversal_regex_message
+ )
+ end
- unless file_name =~ Gitlab::Regex.file_name_regex
+ unless @file_path =~ Gitlab::Regex.file_path_regex
raise_error(
'Your changes could not be committed, because the file name ' +
- Gitlab::Regex.file_name_regex_message
+ Gitlab::Regex.file_path_regex_message
)
end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index a20903c6f02..1960dc7d949 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
module Files
class UpdateService < Files::BaseService
def commit
- repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch)
+ repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, true)
end
end
end
diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb
new file mode 100644
index 00000000000..8f5c3393dfc
--- /dev/null
+++ b/app/services/git_hooks_service.rb
@@ -0,0 +1,28 @@
+class GitHooksService
+ PreReceiveError = Class.new(StandardError)
+
+ def execute(user, repo_path, oldrev, newrev, ref)
+ @repo_path = repo_path
+ @user = Gitlab::ShellEnv.gl_id(user)
+ @oldrev = oldrev
+ @newrev = newrev
+ @ref = ref
+
+ %w(pre-receive update).each do |hook_name|
+ unless run_hook(hook_name)
+ raise PreReceiveError.new("Git operation was rejected by #{hook_name} hook")
+ end
+ end
+
+ yield
+
+ run_hook('post-receive')
+ end
+
+ private
+
+ def run_hook(name)
+ hook = Gitlab::Git::Hook.new(name, @repo_path)
+ hook.trigger(@user, @oldrev, @newrev, @ref)
+ end
+end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 8193b6e192d..f11690aa3f4 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -49,17 +49,14 @@ class GitPushService
elsif push_to_existing_branch?(ref, oldrev)
# Collect data for this git push
@push_commits = project.repository.commits_between(oldrev, newrev)
- project.update_merge_requests(oldrev, newrev, ref, @user)
process_commit_messages(ref)
end
- @push_data = build_push_data(oldrev, newrev, ref)
+ # 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)
- # If CI was disabled but .gitlab-ci.yml file was pushed
- # we enable CI automatically
- if !project.gitlab_ci? && gitlab_ci_yaml?(newrev)
- project.enable_ci(user)
- end
+ @push_data = build_push_data(oldrev, newrev, ref)
EventCreateService.new.push(project, user, @push_data)
project.execute_hooks(@push_data.dup, :push_hooks)
@@ -74,48 +71,30 @@ class GitPushService
def process_commit_messages(ref)
is_default_branch = is_default_branch?(ref)
- @push_commits.each do |commit|
- # Close issues if these commits were pushed to the project's default branch and the commit message matches the
- # closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to
- # a different branch.
- issues_to_close = commit.closes_issues(user)
+ authors = Hash.new do |hash, commit|
+ email = commit.author_email
+ next hash[email] if hash.has_key?(email)
- # Load commit author only if needed.
- # For push with 1k commits it prevents 900+ requests in database
- author = nil
+ hash[email] = commit_user(commit)
+ end
+ @push_commits.each do |commit|
# Keep track of the issues that will be actually closed because they are on a default branch.
# Hence, when creating cross-reference notes, the not-closed issues (on non-default branches)
# will also have cross-reference.
- actually_closed_issues = []
-
- if issues_to_close.present? && is_default_branch
- author ||= commit_user(commit)
- actually_closed_issues = issues_to_close
- issues_to_close.each do |issue|
- Issues::CloseService.new(project, author, {}).execute(issue, commit)
+ closed_issues = []
+
+ if is_default_branch
+ # 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.each do |issue|
+ Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
end
end
- if project.default_issues_tracker?
- create_cross_reference_notes(commit, actually_closed_issues)
- end
- end
- end
-
- def create_cross_reference_notes(commit, issues_to_close)
- # Create cross-reference notes for any other references than those given in issues_to_close.
- # Omit any issues that were referenced in an issue-closing phrase, or have already been
- # mentioned from this commit (probably from this commit being pushed to a different branch).
- refs = commit.references(project, user) - issues_to_close
- refs.reject! { |r| commit.has_mentioned?(r) }
-
- if refs.present?
- author ||= commit_user(commit)
-
- refs.each do |r|
- SystemNoteService.cross_reference(r, commit, author)
- end
+ commit.create_cross_references!(authors[commit], closed_issues)
end
end
@@ -149,10 +128,4 @@ class GitPushService
def commit_user(commit)
commit.author || user
end
-
- def gitlab_ci_yaml?(sha)
- @project.repository.blob_at(sha, '.gitlab-ci.yml')
- rescue Rugged::ReferenceError
- nil
- end
end
diff --git a/app/services/gravatar_service.rb b/app/services/gravatar_service.rb
index 4bee0c26a68..433ecc2df32 100644
--- a/app/services/gravatar_service.rb
+++ b/app/services/gravatar_service.rb
@@ -1,13 +1,13 @@
class GravatarService
include Gitlab::CurrentSettings
- def execute(email, size = nil)
+ def execute(email, size = nil, scale = 2)
if current_application_settings.gravatar_enabled? && email.present?
size = 40 if size.nil? || size <= 0
sprintf gravatar_url,
hash: Digest::MD5.hexdigest(email.strip.downcase),
- size: size,
+ size: size * scale,
email: email.strip
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 15b3825f96a..2556f06e2d3 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -27,7 +27,16 @@ class IssuableBaseService < BaseService
old_branch, new_branch)
end
+ def create_task_status_note(issuable)
+ issuable.updated_tasks.each do |task|
+ SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
+ end
+ end
+
def filter_params(issuable_ability_name = :issue)
+ params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
+ params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
+
ability = :"admin_#{issuable_ability_name}"
unless can?(current_user, ability, project)
@@ -36,4 +45,44 @@ class IssuableBaseService < BaseService
params.delete(:assignee_id)
end
end
+
+ def update(issuable)
+ change_state(issuable)
+ filter_params
+ old_labels = issuable.labels.to_a
+
+ if params.present? && issuable.update_attributes(params.merge(updated_by: current_user))
+ issuable.reset_events_cache
+ handle_common_system_notes(issuable, old_labels: old_labels)
+ handle_changes(issuable)
+ issuable.create_new_cross_references!(current_user)
+ execute_hooks(issuable, 'update')
+ end
+
+ issuable
+ end
+
+ def change_state(issuable)
+ case params.delete(:state_event)
+ when 'reopen'
+ reopen_service.new(project, current_user, {}).execute(issuable)
+ when 'close'
+ close_service.new(project, current_user, {}).execute(issuable)
+ end
+ end
+
+ def handle_common_system_notes(issuable, options = {})
+ if issuable.previous_changes.include?('title')
+ create_title_change_note(issuable, issuable.previous_changes['title'].first)
+ end
+
+ if issuable.previous_changes.include?('description') && issuable.tasks?
+ create_task_status_note(issuable)
+ end
+
+ old_labels = options[:old_labels]
+ if old_labels && (issuable.labels != old_labels)
+ create_labels_note(issuable, issuable.labels - old_labels, old_labels - issuable.labels)
+ end
+ end
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 1ea4b72216c..bcb380d3215 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -10,7 +10,7 @@ module Issues
issue.update_attributes(label_ids: label_params)
notification_service.new_issue(issue, current_user)
event_service.open_issue(issue, current_user)
- issue.create_cross_references!(issue.project, current_user)
+ issue.create_cross_references!(current_user)
execute_hooks(issue, 'open')
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 2fc6ef7f356..a55a04dd5e0 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -1,45 +1,26 @@
module Issues
class UpdateService < Issues::BaseService
def execute(issue)
- case params.delete(:state_event)
- when 'reopen'
- Issues::ReopenService.new(project, current_user, {}).execute(issue)
- when 'close'
- Issues::CloseService.new(project, current_user, {}).execute(issue)
- end
-
- params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
- params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
-
- filter_params
- old_labels = issue.labels.to_a
-
- if params.present? && issue.update_attributes(params.merge(updated_by: current_user))
- issue.reset_events_cache
-
- if issue.labels != old_labels
- create_labels_note(
- issue, issue.labels - old_labels, old_labels - issue.labels)
- end
-
- if issue.previous_changes.include?('milestone_id')
- create_milestone_note(issue)
- end
-
- if issue.previous_changes.include?('assignee_id')
- create_assignee_note(issue)
- notification_service.reassigned_issue(issue, current_user)
- end
+ update(issue)
+ end
- if issue.previous_changes.include?('title')
- create_title_change_note(issue, issue.previous_changes['title'].first)
- end
+ def handle_changes(issue)
+ if issue.previous_changes.include?('milestone_id')
+ create_milestone_note(issue)
+ end
- issue.create_new_cross_references!(issue.project, current_user)
- execute_hooks(issue, 'update')
+ if issue.previous_changes.include?('assignee_id')
+ create_assignee_note(issue)
+ notification_service.reassigned_issue(issue, current_user)
end
+ end
+
+ def reopen_service
+ Issues::ReopenService
+ end
- issue
+ def close_service
+ Issues::CloseService
end
end
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 9651b16462c..009d5a6867e 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -18,7 +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)
- merge_request.create_cross_references!(merge_request.project, 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 98a67c0bc99..d619b72e3c2 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -29,7 +29,7 @@ module MergeRequests
private
def commit
- committer = repository.user_to_comitter(current_user)
+ committer = repository.user_to_committer(current_user)
options = {
message: commit_message,
@@ -38,6 +38,10 @@ module MergeRequests
}
repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options)
+ rescue StandardError => e
+ merge_request.update(merge_error: "Something went wrong during merge")
+ Rails.logger.error(e.message)
+ return false
end
def after_merge
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index aceb8cb9021..8f25c5e2496 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -6,6 +6,7 @@ module MergeRequests
#
class PostMergeService < MergeRequests::BaseService
def execute(merge_request)
+ close_issues(merge_request)
merge_request.mark_as_merged
create_merge_event(merge_request, current_user)
create_note(merge_request)
@@ -15,6 +16,15 @@ module MergeRequests
private
+ def close_issues(merge_request)
+ return unless merge_request.target_branch == project.default_branch
+
+ closed_issues = merge_request.closes_issues(current_user)
+ closed_issues.each do |issue|
+ Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
+ end
+ end
+
def create_merge_event(merge_request, current_user)
EventCreateService.new.merge_mr(merge_request, current_user)
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index e903e48e3cd..e180edb4bf3 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -5,13 +5,20 @@ module MergeRequests
@oldrev, @newrev = oldrev, newrev
@branch_name = Gitlab::Git.ref_name(ref)
- @fork_merge_requests = @project.fork_merge_requests.opened
- @commits = @project.repository.commits_between(oldrev, newrev)
+ find_new_commits
+ # Be sure to close outstanding MRs before reloading them to avoid generating an
+ # empty diff during a manual merge
close_merge_requests
reload_merge_requests
- execute_mr_web_hooks
+
+ # Leave a system note if a branch was deleted/added
+ if branch_added? || branch_removed?
+ comment_mr_branch_presence_changed
+ end
+
comment_mr_with_commits
+ execute_mr_web_hooks
true
end
@@ -31,7 +38,6 @@ module MergeRequests
commit_ids.include?(merge_request.last_commit.id)
end
-
merge_requests.uniq.select(&:source_project).each do |merge_request|
MergeRequests::PostMergeService.
new(merge_request.target_project, @current_user).
@@ -47,7 +53,7 @@ module MergeRequests
# Note: we should update merge requests from forks too
def reload_merge_requests
merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a
- merge_requests += @fork_merge_requests.by_branch(@branch_name).to_a
+ merge_requests += fork_merge_requests.by_branch(@branch_name).to_a
merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request|
@@ -70,13 +76,48 @@ module MergeRequests
end
end
+ def find_new_commits
+ if branch_added?
+ @commits = []
+
+ merge_request = merge_requests_for_source_branch.first
+ return unless merge_request
+
+ last_commit = merge_request.last_commit
+
+ begin
+ # Since any number of commits could have been made to the restored branch,
+ # find the common root to see what has been added.
+ common_ref = @project.repository.merge_base(last_commit.id, @newrev)
+ # If the a commit no longer exists in this repo, gitlab_git throws
+ # a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52
+ @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref
+ rescue
+ end
+ elsif branch_removed?
+ # No commits for a deleted branch.
+ @commits = []
+ else
+ @commits = @project.repository.commits_between(@oldrev, @newrev)
+ end
+ end
+
+ # Add comment about branches being deleted or added to merge requests
+ def comment_mr_branch_presence_changed
+ presence = branch_added? ? :add : :delete
+
+ merge_requests_for_source_branch.each do |merge_request|
+ SystemNoteService.change_branch_presence(
+ merge_request, merge_request.project, @current_user,
+ :source, @branch_name, presence)
+ end
+ end
+
# Add comment about pushing new commits to merge requests
def comment_mr_with_commits
- merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
- merge_requests += @fork_merge_requests.where(source_branch: @branch_name).to_a
- merge_requests = filter_merge_requests(merge_requests)
+ return unless @commits.present?
- merge_requests.each do |merge_request|
+ merge_requests_for_source_branch.each do |merge_request|
mr_commit_ids = Set.new(merge_request.commits.map(&:id))
new_commits, existing_commits = @commits.partition do |commit|
@@ -91,14 +132,7 @@ module MergeRequests
# Call merge request webhook with update branches
def execute_mr_web_hooks
- merge_requests = @project.origin_merge_requests.opened
- .where(source_branch: @branch_name)
- .to_a
- merge_requests += @fork_merge_requests.where(source_branch: @branch_name)
- .to_a
- merge_requests = filter_merge_requests(merge_requests)
-
- merge_requests.each do |merge_request|
+ merge_requests_for_source_branch.each do |merge_request|
execute_hooks(merge_request, 'update')
end
end
@@ -106,5 +140,25 @@ module MergeRequests
def filter_merge_requests(merge_requests)
merge_requests.uniq.select(&:source_project)
end
+
+ def merge_requests_for_source_branch
+ @source_merge_requests ||= begin
+ merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
+ merge_requests += fork_merge_requests.where(source_branch: @branch_name).to_a
+ filter_merge_requests(merge_requests)
+ end
+ end
+
+ def fork_merge_requests
+ @fork_merge_requests ||= @project.fork_merge_requests.opened
+ end
+
+ def branch_added?
+ Gitlab::Git.blank_ref?(@oldrev)
+ end
+
+ def branch_removed?
+ Gitlab::Git.blank_ref?(@newrev)
+ end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 25d79e22e39..5ff2cc03dda 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -11,59 +11,37 @@ module MergeRequests
params.except!(:target_project_id)
params.except!(:source_branch)
- case params.delete(:state_event)
- when 'reopen'
- MergeRequests::ReopenService.new(project, current_user, {}).execute(merge_request)
- when 'close'
- MergeRequests::CloseService.new(project, current_user, {}).execute(merge_request)
- end
-
- params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
- params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
-
- filter_params
- old_labels = merge_request.labels.to_a
-
- if params.present? && merge_request.update_attributes(params.merge(updated_by: current_user))
- merge_request.reset_events_cache
-
- if merge_request.labels != old_labels
- create_labels_note(
- merge_request,
- merge_request.labels - old_labels,
- old_labels - merge_request.labels
- )
- end
-
- if merge_request.previous_changes.include?('target_branch')
- create_branch_change_note(merge_request, 'target',
- merge_request.previous_changes['target_branch'].first,
- merge_request.target_branch)
- end
-
- if merge_request.previous_changes.include?('milestone_id')
- create_milestone_note(merge_request)
- end
+ update(merge_request)
+ end
- if merge_request.previous_changes.include?('assignee_id')
- create_assignee_note(merge_request)
- notification_service.reassigned_merge_request(merge_request, current_user)
- end
+ def handle_changes(merge_request)
+ if merge_request.previous_changes.include?('target_branch')
+ create_branch_change_note(merge_request, 'target',
+ merge_request.previous_changes['target_branch'].first,
+ merge_request.target_branch)
+ end
- if merge_request.previous_changes.include?('title')
- create_title_change_note(merge_request, merge_request.previous_changes['title'].first)
- end
+ if merge_request.previous_changes.include?('milestone_id')
+ create_milestone_note(merge_request)
+ end
- if merge_request.previous_changes.include?('target_branch') ||
- merge_request.previous_changes.include?('source_branch')
- merge_request.mark_as_unchecked
- end
+ if merge_request.previous_changes.include?('assignee_id')
+ create_assignee_note(merge_request)
+ notification_service.reassigned_merge_request(merge_request, current_user)
+ end
- merge_request.create_new_cross_references!(merge_request.project, current_user)
- execute_hooks(merge_request, 'update')
+ if merge_request.previous_changes.include?('target_branch') ||
+ merge_request.previous_changes.include?('source_branch')
+ merge_request.mark_as_unchecked
end
+ end
+
+ def reopen_service
+ MergeRequests::ReopenService
+ end
- merge_request
+ def close_service
+ MergeRequests::CloseService
end
end
end
diff --git a/app/services/milestones/group_service.rb b/app/services/milestones/group_service.rb
deleted file mode 100644
index 11d702f1e7b..00000000000
--- a/app/services/milestones/group_service.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module Milestones
- class GroupService < Milestones::BaseService
- def initialize(project_milestones)
- @project_milestones = project_milestones.group_by(&:title)
- end
-
- def execute
- build(@project_milestones)
- end
-
- def milestone(title)
- if title
- group_milestone = @project_milestones[title].group_by(&:title)
- build(group_milestone).first
- else
- nil
- end
- end
-
- private
-
- def build(milestone)
- milestone.map{ |title, milestones| GroupMilestone.new(title, milestones) }
- end
- end
-end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 482c0444049..a8486e6a5a1 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -8,16 +8,10 @@ module Notes
if note.save
notification_service.new_note(note)
- # Skip system notes, like status changes and cross-references.
- unless note.system
+ # Skip system notes, like status changes and cross-references and awards
+ unless note.system || note.is_award
event_service.leave_note(note, note.author)
-
- # Create a cross-reference note if this Note contains GFM that names an
- # issue, merge request, or commit.
- note.references.each do |mentioned|
- SystemNoteService.cross_reference(mentioned, note.noteable, note.author)
- end
-
+ note.create_cross_references!
execute_hooks(note)
end
end
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index c22a9333ef6..72e2f78008d 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -4,7 +4,7 @@ module Notes
return note unless note.editable?
note.update_attributes(params.merge(updated_by: current_user))
-
+ note.create_new_cross_references!(current_user)
note.reset_events_cache
note
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index e294b23bc23..bdf7b3ad2bb 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -13,14 +13,14 @@ class NotificationService
# even if user disabled notifications
def new_key(key)
if key.user
- mailer.new_ssh_key_email(key.id)
+ mailer.new_ssh_key_email(key.id).deliver_later
end
end
# Always notify user about email added to profile
def new_email(email)
if email.user
- mailer.new_email_email(email.id)
+ mailer.new_email_email(email.id).deliver_later
end
end
@@ -79,17 +79,27 @@ class NotificationService
end
def merge_mr(merge_request, current_user)
- close_resource_email(merge_request, merge_request.target_project, current_user, 'merged_merge_request_email')
+ close_resource_email(
+ merge_request,
+ merge_request.target_project,
+ current_user,
+ 'merged_merge_request_email'
+ )
end
def reopen_mr(merge_request, current_user)
- reopen_resource_email(merge_request, merge_request.target_project, current_user, 'merge_request_status_email', 'reopened')
+ reopen_resource_email(
+ merge_request,
+ merge_request.target_project,
+ current_user, 'merge_request_status_email',
+ 'reopened'
+ )
end
# Notify new user with email after creation
def new_user(user, token = nil)
# Don't email omniauth created users
- mailer.new_user_email(user.id, token) unless user.identities.any?
+ mailer.new_user_email(user.id, token).deliver_later unless user.identities.any?
end
# Notify users on new note in system
@@ -102,6 +112,7 @@ class NotificationService
# ignore gitlab service messages
return true if note.note.start_with?('Status changed to closed')
return true if note.cross_reference? && note.system == true
+ return true if note.is_award
target = note.noteable
@@ -113,7 +124,7 @@ class NotificationService
end
# Add all users participating in the thread (author, assignee, comment authors)
- participants =
+ participants =
if target.respond_to?(:participants)
target.participants(note.author)
else
@@ -134,61 +145,75 @@ class NotificationService
recipients = reject_unsubscribed_users(recipients, note.noteable)
recipients.delete(note.author)
+ recipients = recipients.uniq
# build notify method like 'note_commit_email'
notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
-
recipients.each do |recipient|
- mailer.send(notify_method, recipient.id, note.id)
+ mailer.send(notify_method, recipient.id, note.id).deliver_later
end
end
def invite_project_member(project_member, token)
- mailer.project_member_invited_email(project_member.id, token)
+ mailer.project_member_invited_email(project_member.id, token).deliver_later
end
def accept_project_invite(project_member)
- mailer.project_invite_accepted_email(project_member.id)
+ mailer.project_invite_accepted_email(project_member.id).deliver_later
end
def decline_project_invite(project_member)
- mailer.project_invite_declined_email(project_member.project.id, project_member.invite_email, project_member.access_level, project_member.created_by_id)
+ mailer.project_invite_declined_email(
+ project_member.project.id,
+ project_member.invite_email,
+ project_member.access_level,
+ project_member.created_by_id
+ ).deliver_later
end
def new_project_member(project_member)
- mailer.project_access_granted_email(project_member.id)
+ mailer.project_access_granted_email(project_member.id).deliver_later
end
def update_project_member(project_member)
- mailer.project_access_granted_email(project_member.id)
+ mailer.project_access_granted_email(project_member.id).deliver_later
end
def invite_group_member(group_member, token)
- mailer.group_member_invited_email(group_member.id, token)
+ mailer.group_member_invited_email(group_member.id, token).deliver_later
end
def accept_group_invite(group_member)
- mailer.group_invite_accepted_email(group_member.id)
+ mailer.group_invite_accepted_email(group_member.id).deliver_later
end
def decline_group_invite(group_member)
- mailer.group_invite_declined_email(group_member.group.id, group_member.invite_email, group_member.access_level, group_member.created_by_id)
+ mailer.group_invite_declined_email(
+ group_member.group.id,
+ group_member.invite_email,
+ group_member.access_level,
+ group_member.created_by_id
+ ).deliver_later
end
def new_group_member(group_member)
- mailer.group_access_granted_email(group_member.id)
+ mailer.group_access_granted_email(group_member.id).deliver_later
end
def update_group_member(group_member)
- mailer.group_access_granted_email(group_member.id)
+ mailer.group_access_granted_email(group_member.id).deliver_later
end
- def project_was_moved(project)
+ def project_was_moved(project, old_path_with_namespace)
recipients = project.team.members
recipients = reject_muted_users(recipients, project)
recipients.each do |recipient|
- mailer.project_was_moved_email(project.id, recipient.id)
+ mailer.project_was_moved_email(
+ project.id,
+ recipient.id,
+ old_path_with_namespace
+ ).deliver_later
end
end
@@ -276,35 +301,25 @@ class NotificationService
# Remove users with disabled notifications from array
# Also remove duplications and nil recipients
def reject_muted_users(users, project = nil)
- users = users.to_a.compact.uniq
- users = users.reject(&:blocked?)
-
- users.reject do |user|
- next user.notification.disabled? unless project
-
- member = project.project_members.find_by(user_id: user.id)
-
- if !member && project.group
- member = project.group.group_members.find_by(user_id: user.id)
- end
-
- # reject users who globally disabled notification and has no membership
- next user.notification.disabled? unless member
-
- # reject users who disabled notification in project
- next true if member.notification.disabled?
-
- # reject users who have N_GLOBAL in project and disabled in global settings
- member.notification.global? && user.notification.disabled?
- end
+ reject_users(users, :disabled?, project)
end
# Remove users with notification level 'Mentioned'
def reject_mention_users(users, project = nil)
+ reject_users(users, :mention?, project)
+ end
+
+ # Reject users which method_name from notification object returns true.
+ #
+ # Example:
+ # reject_users(users, :watch?, project)
+ #
+ def reject_users(users, method_name, project = nil)
users = users.to_a.compact.uniq
+ users = users.reject(&:blocked?)
users.reject do |user|
- next user.notification.mention? unless project
+ next user.notification.send(method_name) unless project
member = project.project_members.find_by(user_id: user.id)
@@ -313,19 +328,19 @@ class NotificationService
end
# reject users who globally set mention notification and has no membership
- next user.notification.mention? unless member
+ next user.notification.send(method_name) unless member
# reject users who set mention notification in project
- next true if member.notification.mention?
+ next true if member.notification.send(method_name)
# reject users who have N_MENTION in project and disabled in global settings
- member.notification.global? && user.notification.mention?
+ member.notification.global? && user.notification.send(method_name)
end
end
def reject_unsubscribed_users(recipients, target)
return recipients unless target.respond_to? :subscriptions
-
+
recipients.reject do |user|
subscription = target.subscriptions.find_by_user_id(user.id)
subscription && !subscription.subscribed
@@ -343,12 +358,12 @@ class NotificationService
recipients
end
end
-
+
def new_resource_email(target, project, method)
recipients = build_recipients(target, project, target.author)
recipients.each do |recipient|
- mailer.send(method, recipient.id, target.id)
+ mailer.send(method, recipient.id, target.id).deliver_later
end
end
@@ -356,16 +371,24 @@ class NotificationService
recipients = build_recipients(target, project, current_user)
recipients.each do |recipient|
- mailer.send(method, recipient.id, target.id, current_user.id)
+ mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
end
end
def reassign_resource_email(target, project, current_user, method)
- assignee_id_was = previous_record(target, "assignee_id")
- recipients = build_recipients(target, project, current_user)
+ previous_assignee_id = previous_record(target, "assignee_id")
+ previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
+
+ recipients = build_recipients(target, project, current_user, [previous_assignee])
recipients.each do |recipient|
- mailer.send(method, recipient.id, target.id, assignee_id_was, current_user.id)
+ mailer.send(
+ method,
+ recipient.id,
+ target.id,
+ previous_assignee_id,
+ current_user.id
+ ).deliver_later
end
end
@@ -373,13 +396,15 @@ class NotificationService
recipients = build_recipients(target, project, current_user)
recipients.each do |recipient|
- mailer.send(method, recipient.id, target.id, status, current_user.id)
+ mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later
end
end
- def build_recipients(target, project, current_user)
+ def build_recipients(target, project, current_user, extra_recipients = nil)
recipients = target.participants(current_user)
+ recipients = recipients.concat(extra_recipients).compact.uniq if extra_recipients
+
recipients = add_project_watchers(recipients, project)
recipients = reject_mention_users(recipients, project)
recipients = reject_muted_users(recipients, project)
@@ -393,7 +418,7 @@ class NotificationService
end
def mailer
- Notify.delay
+ Notify
end
def previous_record(object, attribute)
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index e54a13ed6c5..700a1db04d8 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -55,14 +55,16 @@ module Projects
@project.save
if @project.persisted? && !@project.import?
- raise 'Failed to create repository' unless @project.create_repository
+ unless @project.create_repository
+ raise 'Failed to create repository'
+ end
end
end
after_create_actions if @project.persisted?
@project
- rescue => ex
+ rescue
@project.errors.add(:base, "Can't save project. Please try again later")
@project
end
@@ -94,11 +96,7 @@ module Projects
@project.team << [current_user, :master, current_user]
end
- @project.update_column(:last_activity_at, @project.created_at)
-
- if @project.import?
- @project.import_start
- end
+ @project.import_start if @project.import?
end
end
end
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 2e995d6fd51..5da1c7afd92 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -17,8 +17,14 @@ module Projects
new_project = CreateService.new(current_user, new_params).execute
if new_project.persisted?
- if @project.gitlab_ci?
- @project.gitlab_ci_service.fork_registration(new_project, @current_user)
+ if @project.builds_enabled?
+ new_project.enable_ci
+
+ settings = @project.gitlab_ci_project.attributes.select do |attr_name, value|
+ ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name
+ end
+
+ new_project.gitlab_ci_project.update(settings)
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 550ed6897dd..64ea6dd42eb 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -27,6 +27,7 @@ module Projects
def transfer(project, new_namespace)
Project.transaction do
old_path = project.path_with_namespace
+ old_namespace = project.namespace
new_path = File.join(new_namespace.try(:path) || '', project.path)
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present?
@@ -38,7 +39,7 @@ module Projects
project.save!
# Notifications
- project.send_move_instructions
+ project.send_move_instructions(old_path)
# Move main repository
unless gitlab_shell.mv_repository(old_path, new_path)
@@ -51,6 +52,9 @@ module Projects
# clear project cached events
project.reset_events_cache
+ # Move uploads
+ Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+
true
end
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 60235b6be2a..8b5143e1eb7 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -33,17 +33,7 @@ class SystemHooksService
)
end
when Project
- owner = model.owner
-
- data.merge!({
- name: model.name,
- path: model.path,
- path_with_namespace: model.path_with_namespace,
- project_id: model.id,
- owner_name: owner.name,
- owner_email: owner.respond_to?(:email) ? owner.email : "",
- project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase
- })
+ data.merge!(project_data(model))
when User
data.merge!({
name: model.name,
@@ -51,15 +41,7 @@ class SystemHooksService
user_id: model.id
})
when ProjectMember
- data.merge!({
- project_name: model.project.name,
- project_path: model.project.path,
- project_id: model.project.id,
- user_name: model.user.name,
- user_email: model.user.email,
- access_level: model.human_access,
- project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase
- })
+ data.merge!(project_member_data(model))
when Group
owner = model.owner
@@ -71,15 +53,7 @@ class SystemHooksService
owner_email: owner.respond_to?(:email) ? owner.email : nil,
)
when GroupMember
- data.merge!(
- group_name: model.group.name,
- group_path: model.group.path,
- group_id: model.group.id,
- user_name: model.user.name,
- user_email: model.user.email,
- user_id: model.user.id,
- group_access: model.human_access,
- )
+ data.merge!(group_member_data(model))
end
end
@@ -95,4 +69,43 @@ class SystemHooksService
"#{model.class.name.downcase}_#{event.to_s}"
end
end
+
+ def project_data(model)
+ owner = model.owner
+
+ {
+ name: model.name,
+ path: model.path,
+ path_with_namespace: model.path_with_namespace,
+ project_id: model.id,
+ owner_name: owner.name,
+ owner_email: owner.respond_to?(:email) ? owner.email : "",
+ project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase
+ }
+ end
+
+ def project_member_data(model)
+ {
+ project_name: model.project.name,
+ project_path: model.project.path,
+ project_path_with_namespace: model.project.path_with_namespace,
+ project_id: model.project.id,
+ user_name: model.user.name,
+ user_email: model.user.email,
+ access_level: model.human_access,
+ project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase
+ }
+ end
+
+ def group_member_data(model)
+ {
+ group_name: model.group.name,
+ group_path: model.group.path,
+ group_id: model.group.id,
+ user_name: model.user.name,
+ user_email: model.user.email,
+ user_id: model.user.id,
+ group_access: model.human_access,
+ }
+ end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 8253c1f780d..09c159510cd 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -125,7 +125,7 @@ class SystemNoteService
# Returns the created Note object
def self.change_status(noteable, project, author, status, source)
body = "Status changed to #{status}"
- body += " by #{source.gfm_reference}" if source
+ body += " by #{source.gfm_reference(project)}" if source
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -168,6 +168,31 @@ class SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ # Called when a branch in Noteable is added or deleted
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # branch_type - :source or :target
+ # branch - branch name
+ # presence - :add or :delete
+ #
+ # Example Note text:
+ #
+ # "Restored target branch `feature`"
+ #
+ # Returns the created Note object
+ def self.change_branch_presence(noteable, project, author, branch_type, branch, presence)
+ verb =
+ if presence == :add
+ 'restored'
+ else
+ 'deleted'
+ end
+ body = "#{verb} #{branch_type.to_s} branch `#{branch}`".capitalize
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
# Called when a Mentionable references a Noteable
#
# noteable - Noteable object being referenced
@@ -302,7 +327,7 @@ class SystemNoteService
commit_ids = if count == 1
existing_commits.first.short_id
else
- if oldrev
+ if oldrev && !Gitlab::Git.blank_ref?(oldrev)
"#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}"
else
"#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
@@ -316,4 +341,22 @@ class SystemNoteService
"* #{commit_ids} - #{commits_text} from branch `#{branch}`\n"
end
+
+ # Called when the status of a Task has changed
+ #
+ # noteable - Noteable object.
+ # project - Project owning noteable
+ # author - User performing the change
+ # new_task - TaskList::Item object.
+ #
+ # Example Note text:
+ #
+ # "Soandso marked the task Whatever as completed."
+ #
+ # Returns the created Note object
+ def self.change_task_status(noteable, project, author, new_task)
+ status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
+ body = "Marked the task **#{new_task.source}** as #{status_label}"
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
end
diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb
new file mode 100644
index 00000000000..25eb13ef09a
--- /dev/null
+++ b/app/services/update_release_service.rb
@@ -0,0 +1,29 @@
+require_relative 'base_service'
+
+class UpdateReleaseService < BaseService
+ def execute(tag_name, release_description)
+
+ repository = project.repository
+ existing_tag = repository.find_tag(tag_name)
+
+ if existing_tag
+ release = project.releases.find_by(tag: tag_name)
+
+ if release
+ release.update_attributes(description: release_description)
+
+ success(release)
+ else
+ error('Release does not exist', 404)
+ end
+ else
+ error('Tag does not exist', 404)
+ end
+ end
+
+ def success(release)
+ out = super()
+ out[:release] = release
+ out
+ end
+end
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
new file mode 100644
index 00000000000..1dccc39e7e2
--- /dev/null
+++ b/app/uploaders/artifact_uploader.rb
@@ -0,0 +1,50 @@
+# encoding: utf-8
+class ArtifactUploader < CarrierWave::Uploader::Base
+ storage :file
+
+ attr_accessor :build, :field
+
+ def self.artifacts_path
+ Gitlab.config.artifacts.path
+ end
+
+ def self.artifacts_upload_path
+ File.join(self.artifacts_path, 'tmp/uploads/')
+ end
+
+ def self.artifacts_cache_path
+ File.join(self.artifacts_path, 'tmp/cache/')
+ end
+
+ def initialize(build, field)
+ @build, @field = build, field
+ end
+
+ def artifacts_path
+ File.join(build.created_at.utc.strftime('%Y_%m'), build.project.id.to_s, build.id.to_s)
+ end
+
+ def store_dir
+ File.join(ArtifactUploader.artifacts_path, artifacts_path)
+ end
+
+ def cache_dir
+ File.join(ArtifactUploader.artifacts_cache_path, artifacts_path)
+ end
+
+ def file_storage?
+ self.class.storage == CarrierWave::Storage::File
+ end
+
+ def exists?
+ file.try(:exists?)
+ end
+
+ def move_to_cache
+ true
+ end
+
+ def move_to_store
+ true
+ end
+end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index a9691bee46e..a65a896e41e 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,26 +1,11 @@
# encoding: utf-8
class AttachmentUploader < CarrierWave::Uploader::Base
+ include UploaderHelper
+
storage :file
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
-
- def image?
- img_ext = %w(png jpg jpeg gif bmp tiff)
- if file.respond_to?(:extension)
- img_ext.include?(file.extension.downcase)
- else
- # Not all CarrierWave storages respond to :extension
- ext = file.path.split('.').last.downcase
- img_ext.include?(ext)
- end
- rescue
- false
- end
-
- def file_storage?
- self.class.storage == CarrierWave::Storage::File
- end
end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 7cad044555b..6135c3ad96f 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,6 +1,8 @@
# encoding: utf-8
class AvatarUploader < CarrierWave::Uploader::Base
+ include UploaderHelper
+
storage :file
after :store, :reset_events_cache
@@ -9,23 +11,6 @@ class AvatarUploader < CarrierWave::Uploader::Base
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
- def image?
- img_ext = %w(png jpg jpeg gif bmp tiff)
- if file.respond_to?(:extension)
- img_ext.include?(file.extension.downcase)
- else
- # Not all CarrierWave storages respond to :extension
- ext = file.path.split('.').last.downcase
- img_ext.include?(ext)
- end
- rescue
- false
- end
-
- def file_storage?
- self.class.storage == CarrierWave::Storage::File
- end
-
def reset_events_cache(file)
model.reset_events_cache if model.is_a?(User)
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index f9673abbfe8..ac920119a85 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,5 +1,7 @@
# encoding: utf-8
class FileUploader < CarrierWave::Uploader::Base
+ include UploaderHelper
+
storage :file
attr_accessor :project, :secret
@@ -26,23 +28,6 @@ class FileUploader < CarrierWave::Uploader::Base
end
def secure_url
- File.join(Gitlab.config.gitlab.url, @project.path_with_namespace, "uploads", @secret, file.filename)
- end
-
- def file_storage?
- self.class.storage == CarrierWave::Storage::File
- end
-
- def image?
- img_ext = %w(png jpg jpeg gif bmp tiff)
- if file.respond_to?(:extension)
- img_ext.include?(file.extension.downcase)
- else
- # Not all CarrierWave storages respond to :extension
- ext = file.path.split('.').last.downcase
- img_ext.include?(ext)
- end
- rescue
- false
+ File.join("/uploads", @secret, file.filename)
end
end
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
new file mode 100644
index 00000000000..28085b31083
--- /dev/null
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -0,0 +1,29 @@
+# encoding: utf-8
+
+class LfsObjectUploader < CarrierWave::Uploader::Base
+ storage :file
+
+ def store_dir
+ "#{Gitlab.config.lfs.storage_path}/#{model.oid[0,2]}/#{model.oid[2,2]}"
+ end
+
+ def cache_dir
+ "#{Gitlab.config.lfs.storage_path}/tmp/cache"
+ end
+
+ def move_to_cache
+ true
+ end
+
+ def move_to_store
+ true
+ end
+
+ def exists?
+ file.try(:exists?)
+ end
+
+ def filename
+ model.oid[4..-1]
+ end
+end
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
new file mode 100644
index 00000000000..5ef440f3367
--- /dev/null
+++ b/app/uploaders/uploader_helper.rb
@@ -0,0 +1,19 @@
+# Extra methods for uploader
+module UploaderHelper
+ def image?
+ img_ext = %w(png jpg jpeg gif bmp tiff)
+ if file.respond_to?(:extension)
+ img_ext.include?(file.extension.downcase)
+ else
+ # Not all CarrierWave storages respond to :extension
+ ext = file.path.split('.').last.downcase
+ img_ext.include?(ext)
+ end
+ rescue
+ false
+ end
+
+ def file_storage?
+ self.class.storage == CarrierWave::Storage::File
+ end
+end
diff --git a/app/validators/color_validator.rb b/app/validators/color_validator.rb
new file mode 100644
index 00000000000..571d0007aa2
--- /dev/null
+++ b/app/validators/color_validator.rb
@@ -0,0 +1,20 @@
+# ColorValidator
+#
+# Custom validator for web color codes. It requires the leading hash symbol and
+# will accept RGB triplet or hexadecimal formats.
+#
+# Example:
+#
+# class User < ActiveRecord::Base
+# validates :background_color, allow_blank: true, color: true
+# end
+#
+class ColorValidator < ActiveModel::EachValidator
+ PATTERN = /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/.freeze
+
+ def validate_each(record, attribute, value)
+ unless value =~ PATTERN
+ record.errors.add(attribute, "must be a valid color code")
+ end
+ end
+end
diff --git a/lib/email_validator.rb b/app/validators/email_validator.rb
index f509f0a5843..b35af100803 100644
--- a/lib/email_validator.rb
+++ b/app/validators/email_validator.rb
@@ -1,3 +1,5 @@
+# EmailValidator
+#
# Based on https://github.com/balexand/email_validator
#
# Extended to use only strict mode with following allowed characters:
@@ -6,15 +8,10 @@
# See http://www.remote.org/jochen/mail/info/chars.html
#
class EmailValidator < ActiveModel::EachValidator
- @@default_options = {}
-
- def self.default_options
- @@default_options
- end
+ PATTERN = /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i.freeze
def validate_each(record, attribute, value)
- options = @@default_options.merge(self.options)
- unless value =~ /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i
+ unless value =~ PATTERN
record.errors.add(attribute, options[:message] || :invalid)
end
end
diff --git a/app/validators/line_code_validator.rb b/app/validators/line_code_validator.rb
new file mode 100644
index 00000000000..ed29e5aeb67
--- /dev/null
+++ b/app/validators/line_code_validator.rb
@@ -0,0 +1,12 @@
+# LineCodeValidator
+#
+# Custom validator for GitLab line codes.
+class LineCodeValidator < ActiveModel::EachValidator
+ PATTERN = /\A[a-z0-9]+_\d+_\d+\z/.freeze
+
+ def validate_each(record, attribute, value)
+ unless value =~ PATTERN
+ record.errors.add(attribute, "must be a valid line code")
+ end
+ end
+end
diff --git a/app/validators/namespace_name_validator.rb b/app/validators/namespace_name_validator.rb
new file mode 100644
index 00000000000..2e51af2982d
--- /dev/null
+++ b/app/validators/namespace_name_validator.rb
@@ -0,0 +1,10 @@
+# NamespaceNameValidator
+#
+# Custom validator for GitLab namespace name strings.
+class NamespaceNameValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value =~ Gitlab::Regex.namespace_name_regex
+ record.errors.add(attribute, Gitlab::Regex.namespace_name_regex_message)
+ end
+ end
+end
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
new file mode 100644
index 00000000000..10e35ce665a
--- /dev/null
+++ b/app/validators/namespace_validator.rb
@@ -0,0 +1,50 @@
+# NamespaceValidator
+#
+# Custom validator for GitLab namespace values.
+#
+# Values are checked for formatting and exclusion from a list of reserved path
+# names.
+class NamespaceValidator < ActiveModel::EachValidator
+ RESERVED = %w(
+ admin
+ all
+ assets
+ ci
+ dashboard
+ files
+ groups
+ help
+ hooks
+ issues
+ merge_requests
+ notes
+ profile
+ projects
+ public
+ repository
+ s
+ search
+ services
+ snippets
+ teams
+ u
+ unsubscribes
+ users
+ ).freeze
+
+ def validate_each(record, attribute, value)
+ unless value =~ Gitlab::Regex.namespace_regex
+ record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
+ end
+
+ if reserved?(value)
+ record.errors.add(attribute, "#{value} is a reserved name")
+ end
+ end
+
+ private
+
+ def reserved?(value)
+ RESERVED.include?(value)
+ end
+end
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
new file mode 100644
index 00000000000..2848b9cd33d
--- /dev/null
+++ b/app/validators/url_validator.rb
@@ -0,0 +1,36 @@
+# UrlValidator
+#
+# Custom validator for URLs.
+#
+# By default, only URLs for the HTTP(S) protocols will be considered valid.
+# Provide a `:protocols` option to configure accepted protocols.
+#
+# Example:
+#
+# class User < ActiveRecord::Base
+# validates :personal_url, url: true
+#
+# validates :ftp_url, url: { protocols: %w(ftp) }
+#
+# validates :git_url, url: { protocols: %w(http https ssh git) }
+# end
+#
+class UrlValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless valid_url?(value)
+ record.errors.add(attribute, "must be a valid URL")
+ end
+ end
+
+ private
+
+ def default_options
+ @default_options ||= { protocols: %w(http https) }
+ end
+
+ def valid_url?(value)
+ options = default_options.merge(self.options)
+
+ value =~ /\A#{URI.regexp(options[:protocols])}\z/
+ end
+end
diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml
new file mode 100644
index 00000000000..619533e09a7
--- /dev/null
+++ b/app/views/abuse_report_mailer/notify.html.haml
@@ -0,0 +1,11 @@
+%p
+ #{link_to @abuse_report.user.name, user_url(@abuse_report.user)}
+ (@#{@abuse_report.user.username}) was reported for abuse by
+ #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)}
+ (@#{@abuse_report.reporter.username}).
+
+%blockquote
+ = @abuse_report.message
+
+%p
+ = link_to "View details", abuse_reports_url
diff --git a/app/views/abuse_report_mailer/notify.text.haml b/app/views/abuse_report_mailer/notify.text.haml
new file mode 100644
index 00000000000..7dacf857035
--- /dev/null
+++ b/app/views/abuse_report_mailer/notify.text.haml
@@ -0,0 +1,5 @@
+#{@abuse_report.user.name} (@#{@abuse_report.user.username}) was reported for abuse by #{@abuse_report.reporter.name} (@#{@abuse_report.reporter.username}).
+\
+> #{@abuse_report.message}
+\
+View details: #{admin_abuse_reports_url}
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 2e8746146d1..40a5fe4628b 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -2,16 +2,17 @@
%h3.page-title Abuse Reports
%hr
- if @abuse_reports.present?
- %table.table
- %thead
- %tr
- %th Reported by
- %th Reported at
- %th Message
- %th User
- %th Primary action
- %th
- = render @abuse_reports
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th Reported by
+ %th Reported at
+ %th Message
+ %th User
+ %th Primary action
+ %th
+ = render @abuse_reports
= paginate @abuse_reports
- else
%h4 There are no abuse reports
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 143cd10c543..ddaf0e0e8ff 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -47,6 +47,12 @@
= f.label :version_check_enabled do
= f.check_box :version_check_enabled
Version check enabled
+ .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'
+ .help-block
+ Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
%fieldset
%legend Account and Limit Settings
@@ -129,9 +135,14 @@
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
- = f.label :ci_enabled do
- = f.check_box :ci_enabled
- Disable to prevent CI usage until rake ci:migrate is run (8.0 only)
+ = f.label :shared_runners_enabled do
+ = f.check_box :shared_runners_enabled
+ Enable shared runners for a new projects
+
+ .form-group
+ = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_artifacts_size, class: 'form-control'
.form-actions
= f.submit 'Save', class: 'btn btn-primary'
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index fc921a966f3..f8cd98f0ec4 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -2,7 +2,7 @@
%h3.page-title
System OAuth applications
%p.light
- System OAuth application does not belong to certain user and can be managed only by admins
+ System OAuth applications don't belong to any user and can only be managed by admins
%hr
%p= link_to 'New Application', new_admin_application_path, class: 'btn btn-success'
%table.table.table-striped
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 0ea2ffeda99..3eb9d61972b 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -3,25 +3,26 @@
Application: #{@application.name}
-%table.table
- %tr
- %td
- Application Id
- %td
- %code#application_id= @application.uid
- %tr
- %td
- Secret:
- %td
- %code#secret= @application.secret
+.table-holder
+ %table.table
+ %tr
+ %td
+ Application Id
+ %td
+ %code#application_id= @application.uid
+ %tr
+ %td
+ Secret:
+ %td
+ %code#secret= @application.secret
- %tr
- %td
- Callback url
- %td
- - @application.redirect_uri.split.each do |uri|
- %div
- %span.monospace= uri
+ %tr
+ %td
+ Callback url
+ %td
+ - @application.redirect_uri.split.each do |uri|
+ %div
+ %span.monospace= uri
.form-actions
= link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 3a01e115109..de5bc050cf0 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -12,24 +12,25 @@
%i.fa.fa-exclamation-triangle
There are no running sidekiq processes. Please restart GitLab
- else
- %table.table
- %thead
- %th USER
- %th PID
- %th CPU
- %th MEM
- %th STATE
- %th START
- %th COMMAND
- %tbody
- - @sidekiq_processes.each do |process|
- - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/)
- - data = process.strip.split(' ')
- %tr
- %td= gitlab_config.user
- - 5.times do
- %td= data.shift
- %td= data.join(' ')
+ .table-holder
+ %table.table
+ %thead
+ %th USER
+ %th PID
+ %th CPU
+ %th MEM
+ %th STATE
+ %th START
+ %th COMMAND
+ %tbody
+ - @sidekiq_processes.each do |process|
+ - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/)
+ - data = process.strip.split(' ')
+ %tr
+ %td= gitlab_config.user
+ - 5.times do
+ %td= data.shift
+ %td= data.join(' ')
.clearfix
%p
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index 2bf1689cbc6..841e6971fb2 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -5,22 +5,23 @@
.panel-head-actions
= link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm"
- if @deploy_keys.any?
- %table.table
- %thead.panel-heading
- %tr
- %th Title
- %th Fingerprint
- %th Added at
- %th
- %tbody
- - @deploy_keys.each do |deploy_key|
+ .table-holder
+ %table.table
+ %thead.panel-heading
%tr
- %td
- %strong= deploy_key.title
- %td
- %code.key-fingerprint= deploy_key.fingerprint
- %td
- %span.cgray
- added #{time_ago_with_tooltip(deploy_key.created_at)}
- %td
- = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right"
+ %th Title
+ %th Fingerprint
+ %th Added at
+ %th
+ %tbody
+ - @deploy_keys.each do |deploy_key|
+ %tr
+ %td
+ %strong= deploy_key.title
+ %td
+ %code.key-fingerprint= deploy_key.fingerprint
+ %td
+ %span.cgray
+ added #{time_ago_with_tooltip(deploy_key.created_at)}
+ %td
+ = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right"
diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml
index ae57e3adc4d..8358a14445b 100644
--- a/app/views/admin/identities/index.html.haml
+++ b/app/views/admin/identities/index.html.haml
@@ -2,12 +2,13 @@
= render 'admin/users/head'
- if @identities.present?
- %table.table
- %thead
- %tr
- %th Provider
- %th Identifier
- %th
- = render @identities
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th Provider
+ %th Identifier
+ %th
+ = render @identities
- else
%h4 This user has no identities
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index ad58a3837f6..eaa94ed9e36 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -12,7 +12,7 @@
.col-sm-10
= f.text_field :title, class: "form-control", required: true
.form-group
- = f.label :color, "Background Color", class: 'control-label'
+ = f.label :color, "Background color", class: 'control-label'
.col-sm-10
.input-group
.input-group-addon.label-color-preview &nbsp;
@@ -31,5 +31,5 @@
= f.submit 'Save', class: 'btn btn-save js-save-button'
= link_to "Cancel", admin_labels_path, class: 'btn btn-cancel'
-:coffeescript
- new Labels
+:javascript
+ new Labels();
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index 596e06243dd..e3ccbf6c3a8 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -2,4 +2,4 @@
= render_colored_label(label)
.pull-right
= link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
- = link_to 'Remove', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
+ = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
diff --git a/app/views/admin/labels/edit.html.haml b/app/views/admin/labels/edit.html.haml
index 45c62a76259..309aedceded 100644
--- a/app/views/admin/labels/edit.html.haml
+++ b/app/views/admin/labels/edit.html.haml
@@ -1,9 +1,5 @@
- page_title "Edit", @label.name, "Labels"
-%h3
- Edit label
- %span.light #{@label.name}
-.back-link
- = link_to admin_labels_path do
- &larr; To labels list
+%h3.page-title
+ Edit Label
%hr
= render 'form'
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index 8b11c28c56e..d67454c03e7 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -12,5 +12,5 @@
= paginate @labels, theme: 'gitlab'
- else
.light-well
- .nothing-here-block There are no any labels yet
- \ No newline at end of file
+ .nothing-here-block There are no labels yet
+
diff --git a/app/views/admin/labels/new.html.haml b/app/views/admin/labels/new.html.haml
index 8d298ad20f7..0135ad0723d 100644
--- a/app/views/admin/labels/new.html.haml
+++ b/app/views/admin/labels/new.html.haml
@@ -1,7 +1,5 @@
- page_title "New Label"
-%h3 New label
-.back-link
- = link_to admin_labels_path do
- &larr; To labels list
+%h3.page-title
+ New Label
%hr
= render 'form'
diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml
index e2377291142..6a5986f496a 100644
--- a/app/views/admin/services/index.html.haml
+++ b/app/views/admin/services/index.html.haml
@@ -2,22 +2,23 @@
%h3.page-title Service templates
%p.light Service template allows you to set default values for project services
-%table.table
- %thead
- %tr
- %th
- %th Service
- %th Description
- %th Last edit
- - @services.sort_by(&:title).each do |service|
- %tr
- %td
- = icon("copy", class: 'clgray')
- %td
- = link_to edit_admin_application_settings_service_path(service.id) do
- %strong= service.title
- %td
- = service.description
- %td.light
- = time_ago_in_words service.updated_at
- ago
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %th
+ %th Service
+ %th Description
+ %th Last edit
+ - @services.sort_by(&:title).each do |service|
+ %tr
+ %td
+ = icon("copy", class: 'clgray')
+ %td
+ = link_to edit_admin_application_settings_service_path(service.id) do
+ %strong= service.title
+ %td
+ = service.description
+ %td.light
+ = time_ago_in_words service.updated_at
+ ago
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 9d5e934c8ba..5e17b018163 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -6,6 +6,8 @@
%span.cred (Admin)
.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
%i.fa.fa-pencil-square-o
Edit
diff --git a/app/views/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml
index 90d9980c85c..7d11edc79e2 100644
--- a/app/views/users/_profile.html.haml
+++ b/app/views/admin/users/_profile.html.haml
@@ -16,11 +16,11 @@
- unless user.linkedin.blank?
%li
%span.light LinkedIn:
- %strong= link_to user.linkedin, "http://www.linkedin.com/in/#{user.linkedin}"
+ %strong= link_to user.linkedin, "https://www.linkedin.com/in/#{user.linkedin}"
- unless user.twitter.blank?
%li
%span.light Twitter:
- %strong= link_to user.twitter, "http://www.twitter.com/#{user.twitter}"
+ %strong= link_to user.twitter, "https://twitter.com/#{user.twitter}"
- unless user.website_url.blank?
%li
%span.light Website:
diff --git a/app/views/users/_projects.html.haml b/app/views/admin/users/_projects.html.haml
index a126a858ea8..a126a858ea8 100644
--- a/app/views/users/_projects.html.haml
+++ b/app/views/admin/users/_projects.html.haml
diff --git a/app/views/admin/users/edit.html.haml b/app/views/admin/users/edit.html.haml
index a8837d74dd9..3b6fd71500d 100644
--- a/app/views/admin/users/edit.html.haml
+++ b/app/views/admin/users/edit.html.haml
@@ -1,8 +1,5 @@
- page_title "Edit", @user.name, "Users"
%h3.page-title
Edit user: #{@user.name}
-.back-link
- = link_to admin_user_path(@user) do
- &larr; Back to user page
%hr
= render 'form'
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index e3698ac1c46..bc08458312c 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -32,7 +32,7 @@
%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'
+ = 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
@@ -54,19 +54,19 @@
%b.caret
%ul.dropdown-menu
%li
- = link_to admin_users_path(sort: sort_value_name) do
+ = 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) do
+ = 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) do
+ = 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) do
+ = 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) do
+ = 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) do
+ = 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) do
+ = 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"
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 0d7a1a25a80..b655b2a15f5 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -14,7 +14,7 @@
.row
.col-md-6
- if @personal_projects.present?
- = render 'users/projects', projects: @personal_projects
+ = render 'admin/users/projects', projects: @personal_projects
- else
.nothing-here-block This user has no personal projects.
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index a383ea57384..0848504b7a6 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -8,13 +8,13 @@
= @user.name
%ul.well-list
%li
- = image_tag avatar_icon(@user.email, 60), class: "avatar s60"
+ = image_tag avatar_icon(@user, 60), class: "avatar s60"
%li
%span.light Profile page:
%strong
= link_to user_path(@user) do
= @user.username
- = render 'users/profile', user: @user
+ = render 'admin/users/profile', user: @user
.panel.panel-default
.panel-heading
diff --git a/app/views/ci/admin/builds/_build.html.haml b/app/views/ci/admin/builds/_build.html.haml
index 778d51d03be..2df58713214 100644
--- a/app/views/ci/admin/builds/_build.html.haml
+++ b/app/views/ci/admin/builds/_build.html.haml
@@ -1,14 +1,16 @@
+- gl_project = build.project.gl_project
- if build.commit && build.project
%tr.build
%td.build-link
- = link_to ci_project_build_url(build.project, build) do
+ = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do
%strong #{build.id}
%td.status
= ci_status_with_icon(build.status)
%td.commit-link
- = commit_link(build.commit)
+ = link_to ci_status_path(build.commit) do
+ %strong #{build.commit.short_sha}
%td.runner
- if build.runner
diff --git a/app/views/ci/admin/events/index.html.haml b/app/views/ci/admin/events/index.html.haml
index f9ab0994304..5a5b4dc7c35 100644
--- a/app/views/ci/admin/events/index.html.haml
+++ b/app/views/ci/admin/events/index.html.haml
@@ -1,17 +1,18 @@
-%table.table
- %thead
- %tr
- %th User ID
- %th Description
- %th When
- - @events.each do |event|
- %tr
- %td
- = event.user_id
- %td
- = event.description
- %td.light
- = time_ago_in_words event.updated_at
- ago
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %th User ID
+ %th Description
+ %th When
+ - @events.each do |event|
+ %tr
+ %td
+ = event.user_id
+ %td
+ = event.description
+ %td.light
+ = time_ago_in_words event.updated_at
+ ago
-= paginate @events \ No newline at end of file
+= paginate @events
diff --git a/app/views/ci/admin/projects/_project.html.haml b/app/views/ci/admin/projects/_project.html.haml
index c461206c72a..a342d6e1cf0 100644
--- a/app/views/ci/admin/projects/_project.html.haml
+++ b/app/views/ci/admin/projects/_project.html.haml
@@ -1,4 +1,4 @@
-- last_commit = project.last_commit
+- last_commit = project.commits.last
%tr
%td
= project.id
diff --git a/app/views/ci/admin/projects/index.html.haml b/app/views/ci/admin/projects/index.html.haml
index dc7b041473b..0da8547924b 100644
--- a/app/views/ci/admin/projects/index.html.haml
+++ b/app/views/ci/admin/projects/index.html.haml
@@ -1,15 +1,16 @@
-%table.table
- %thead
- %tr
- %th ID
- %th Name
- %th Last build
- %th Access
- %th Builds
- %th
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %th ID
+ %th Name
+ %th Last build
+ %th Access
+ %th Builds
+ %th
- - @projects.each do |project|
- = render "ci/admin/projects/project", project: project
+ - @projects.each do |project|
+ = render "ci/admin/projects/project", project: project
= paginate @projects
diff --git a/app/views/ci/admin/runner_projects/index.html.haml b/app/views/ci/admin/runner_projects/index.html.haml
index f049b4f4c4e..6b4e3b2cb38 100644
--- a/app/views/ci/admin/runner_projects/index.html.haml
+++ b/app/views/ci/admin/runner_projects/index.html.haml
@@ -1,5 +1,5 @@
%p.lead
- To register new runner visit #{link_to 'this page ', ci_runners_path}
+ To register a new runner visit #{link_to 'this page ', ci_runners_path}
.row
.col-md-8
diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml
index b9d6703ff41..bacaccfbffa 100644
--- a/app/views/ci/admin/runners/index.html.haml
+++ b/app/views/ci/admin/runners/index.html.haml
@@ -1,5 +1,5 @@
%p.lead
- %span To register new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication.
+ %span To register a new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication.
%code #{GitlabCi::REGISTRATION_TOKEN}
.bs-callout
@@ -21,13 +21,13 @@
\- run builds from assigned projects
%li
%span.label.label-danger paused
- \- runner will not receive any new build
+ \- runner will not receive any new builds
.append-bottom-20.clearfix
.pull-left
= form_tag ci_admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
.form-group
- = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token'
+ = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false
= submit_tag 'Search', class: 'btn'
.pull-right.light
@@ -35,18 +35,19 @@
%br
-%table.table
- %thead
- %tr
- %th Type
- %th Runner token
- %th Description
- %th Projects
- %th Builds
- %th Tags
- %th Last contact
- %th
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %th Type
+ %th Runner token
+ %th Description
+ %th Projects
+ %th Builds
+ %th Tags
+ %th Last contact
+ %th
- - @runners.each do |runner|
- = render "ci/admin/runners/runner", runner: runner
+ - @runners.each do |runner|
+ = render "ci/admin/runners/runner", runner: runner
= paginate @runners
diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml
index 09905e0eb47..fd3d33d657b 100644
--- a/app/views/ci/admin/runners/show.html.haml
+++ b/app/views/ci/admin/runners/show.html.haml
@@ -13,13 +13,13 @@
- if @runner.shared?
.bs-callout.bs-callout-success
- %h4 This runner will process build from ALL UNASSIGNED projects
+ %h4 This runner will process builds from ALL UNASSIGNED projects
%p
If you want runners to build only specific projects, enable them in the table below.
Keep in mind that this is a one way transition.
- else
.bs-callout.bs-callout-info
- %h4 This runner will process build only from ASSIGNED projects
+ %h4 This runner will process builds only from ASSIGNED projects
%p You can't make this a shared runner.
%hr
= form_for @runner, url: ci_admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f|
@@ -37,7 +37,7 @@
= label_tag :tag_list, class: 'control-label' do
Tags
.col-sm-10
- = f.text_field :tag_list, class: 'form-control'
+ = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control'
.help-block You can setup builds to only use runners with specific tags
.form-actions
= f.submit 'Save', class: 'btn btn-save'
@@ -53,13 +53,14 @@
%th
- @runner.runner_projects.each do |runner_project|
- project = runner_project.project
- %tr.alert-info
- %td
- %strong
- = project.name
- %td
- .pull-right
- = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
+ - if project.gl_project
+ %tr.alert-info
+ %td
+ %strong
+ = project.name
+ %td
+ .pull-right
+ = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
%table.table
%thead
@@ -76,7 +77,7 @@
%td
= form_tag ci_admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do
.form-group
- = search_field_tag :search, params[:search], class: 'form-control'
+ = search_field_tag :search, params[:search], class: 'form-control', spellcheck: false
= submit_tag 'Search', class: 'btn'
%td
@@ -96,22 +97,33 @@
%table.builds.runner-builds
%thead
%tr
+ %th Build ID
%th Status
%th Project
%th Commit
%th Finished at
- @builds.each do |build|
+ - gl_project = build.gl_project
%tr.build
+ %td.id
+ - if gl_project
+ = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do
+ = build.id
+ - else
+ = build.id
+
%td.status
= ci_status_with_icon(build.status)
%td.status
- = build.project.name
+ - if gl_project
+ = gl_project.name_with_namespace
%td.build-link
- = link_to ci_project_build_path(build.project, build) do
- %strong #{build.short_sha}
+ - if gl_project
+ = link_to ci_status_path(build.commit) do
+ %strong #{build.commit.short_sha}
%td.timestamp
- if build.finished_at
diff --git a/app/views/ci/builds/_build.html.haml b/app/views/ci/builds/_build.html.haml
deleted file mode 100644
index 515b862e992..00000000000
--- a/app/views/ci/builds/_build.html.haml
+++ /dev/null
@@ -1,45 +0,0 @@
-%tr.build
- %td.status
- = ci_status_with_icon(build.status)
-
- %td.build-link
- = link_to ci_project_build_path(build.project, build) do
- %strong Build ##{build.id}
-
- %td
- = build.stage
-
- %td
- = build.name
- .pull-right
- - if build.tags.any?
- - build.tag_list.each do |tag|
- %span.label.label-primary
- = tag
- - if build.trigger_request
- %span.label.label-info triggered
- - if build.allow_failure
- %span.label.label-danger allowed to fail
-
- %td.duration
- - if build.duration
- #{duration_in_words(build.finished_at, build.started_at)}
-
- %td.timestamp
- - if build.finished_at
- %span #{time_ago_in_words build.finished_at} ago
-
- - if build.project.coverage_enabled?
- %td.coverage
- - if build.coverage
- #{build.coverage}%
-
- %td
- - if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project)
- .pull-right
- - if build.active?
- = link_to cancel_ci_project_build_path(build.project, build, return_to: request.original_url), title: 'Cancel build' do
- %i.fa.fa-remove.cred
- - elsif build.commands.present?
- = link_to retry_ci_project_build_path(build.project, build, return_to: request.original_url), method: :post, title: 'Retry build' do
- %i.fa.fa-repeat
diff --git a/app/views/ci/builds/show.html.haml b/app/views/ci/builds/show.html.haml
deleted file mode 100644
index 839dbf5c554..00000000000
--- a/app/views/ci/builds/show.html.haml
+++ /dev/null
@@ -1,170 +0,0 @@
-#up-build-trace
-- if @commit.matrix?
- %ul.center-top-menu
- - @commit.builds_without_retry_sorted.each do |build|
- %li{class: ('active' if build == @build) }
- = link_to ci_project_build_url(@project, build) do
- = ci_icon_for_status(build.status)
- %span
- - if build.name
- = build.name
- - else
- = build.id
-
-
- - unless @commit.builds_without_retry.include?(@build)
- %li.active
- %a
- Build ##{@build.id}
- &middot;
- %i.fa.fa-warning-sign
- This build was retried.
-
-.gray-content-block
- .build-head
- %h4
- - if @build.commit.tag?
- Build for tag
- %code #{@build.ref}
- - else
- Build for commit
- %strong.monospace= commit_link(@build.commit)
- from
-
- = link_to ci_project_path(@build.project, ref: @build.ref) do
- %strong.monospace= "#{@build.ref}"
-
- - if @build.duration
- .pull-right
- %span
- %i.fa.fa-time
- #{duration_in_words(@build.finished_at, @build.started_at)}
-
- .clearfix
- = ci_status_with_icon(@build.status)
- .pull-right
- = @build.updated_at.stamp('19:00 Aug 27')
-
-.row.prepend-top-default
- .col-md-9
- .clearfix
- - if @build.active?
- .autoscroll-container
- %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
- .clearfix
- .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
- %div#down-build-trace
-
- .col-md-3
- - if @build.coverage
- .build-widget
- %h4.title
- Test coverage
- %h1 #{@build.coverage}%
-
-
- .build-widget
- %h4.title
- Build
- - if current_user && can?(current_user, :manage_builds, gl_project)
- .pull-right
- - if @build.active?
- = link_to "Cancel", cancel_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-danger'
- - elsif @build.commands.present?
- = link_to "Retry", retry_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-primary', method: :post
-
- - if @build.duration
- %p
- %span.attr-name Duration:
- #{duration_in_words(@build.finished_at, @build.started_at)}
- %p
- %span.attr-name Created:
- #{time_ago_in_words(@build.created_at)} ago
- - if @build.finished_at
- %p
- %span.attr-name Finished:
- #{time_ago_in_words(@build.finished_at)} ago
- %p
- %span.attr-name Runner:
- - if @build.runner && current_user && current_user.admin
- \#{link_to "##{@build.runner.id}", ci_admin_runner_path(@build.runner.id)}
- - elsif @build.runner
- \##{@build.runner.id}
-
- - if @build.trigger_request
- .build-widget
- %h4.title
- Trigger
-
- %p
- %span.attr-name Token:
- #{@build.trigger_request.trigger.short_token}
-
- - if @build.trigger_request.variables
- %p
- %span.attr-name Variables:
-
- %code
- - @build.trigger_request.variables.each do |key, value|
- #{key}=#{value}
-
- .build-widget
- %h4.title
- Commit
- .pull-right
- %small #{build_commit_link @build}
-
- - if @build.commit.compare?
- %p
- %span.attr-name Compare:
- #{build_compare_link @build}
- %p
- %span.attr-name Branch:
- #{build_ref_link @build}
- %p
- %span.attr-name Author:
- #{@build.commit.git_author_name}
- %p
- %span.attr-name Message:
- #{@build.commit.git_commit_message}
-
- - if @build.tags.any?
- .build-widget
- %h4.title
- Tags
- - @build.tag_list.each do |tag|
- %span.label.label-primary
- = tag
-
- - if @builds.present?
- .build-widget
- %h4.title #{pluralize(@builds.count, "other build")} for #{@build.short_sha}:
- %table.builds
- - @builds.each_with_index do |build, i|
- %tr.build
- %td
- = ci_icon_for_status(build.status)
- %td
- = link_to ci_project_build_url(@project, build) do
- - if build.name
- = build.name
- - else
- %span ##{build.id}
-
- %td.status= build.status
-
-
- = paginate @builds
-
-
-:javascript
- new CiBuild("#{ci_project_build_url(@project, @build)}", "#{@build.status}")
diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml
index 1eacfca944f..b24a3b826cf 100644
--- a/app/views/ci/commits/_commit.html.haml
+++ b/app/views/ci/commits/_commit.html.haml
@@ -7,7 +7,7 @@
%td.build-link
- = link_to ci_project_ref_commits_path(commit.project, commit.ref, commit.sha) do
+ = link_to ci_status_path(commit) do
%strong #{commit.short_sha}
%td.build-message
@@ -16,7 +16,8 @@
%td.build-branch
- unless @ref
%span
- = link_to truncate(commit.ref, length: 25), ci_project_path(@project, ref: commit.ref)
+ - commit.refs.each do |ref|
+ = link_to truncate(ref, length: 25), ci_project_path(@project, ref: ref)
%td.duration
- if commit.duration > 0
diff --git a/app/views/ci/commits/show.html.haml b/app/views/ci/commits/show.html.haml
deleted file mode 100644
index 8f38aa84676..00000000000
--- a/app/views/ci/commits/show.html.haml
+++ /dev/null
@@ -1,87 +0,0 @@
-.commit-info
- .append-bottom-20
- = ci_status_with_icon(@commit.status)
-
- .gray-content-block.middle-block
- %pre.commit-message
- #{@commit.git_commit_message}
-
- .gray-content-block.second-block
- .row
- .col-sm-6
- - if @commit.compare?
- %p
- %span.attr-name Compare:
- #{gitlab_compare_link(@project, @commit.short_before_sha, @commit.short_sha)}
- - else
- %p
- %span.attr-name Commit:
- #{gitlab_commit_link(@project, @commit.sha)}
-
- %p
- %span.attr-name Branch:
- #{gitlab_ref_link(@project, @commit.ref)}
- .col-sm-6
- %p
- %span.attr-name Author:
- #{@commit.git_author_name} (#{@commit.git_author_email})
- - if @commit.created_at
- %p
- %span.attr-name Created at:
- #{@commit.created_at.to_s(:short)}
-
-- if current_user && can?(current_user, :manage_builds, gl_project)
- .pull-right
- - if @commit.builds.running_or_pending.any?
- = link_to "Cancel", cancel_ci_project_ref_commits_path(@project, @commit.ref, @commit.sha), class: 'btn btn-sm btn-danger'
-
-
-- if @commit.yaml_errors.present?
- .bs-callout.bs-callout-danger
- %h4 Found errors in your .gitlab-ci.yml:
- %ul
- - @commit.yaml_errors.split(",").each do |error|
- %li= error
-
-- unless @commit.push_data[:ci_yaml_file]
- .bs-callout.bs-callout-warning
- \.gitlab-ci.yml not found in this commit
-
-%h3
- Builds
- - if @commit.duration > 0
- %small.pull-right
- %i.fa.fa-time
- #{time_interval_in_words @commit.duration}
-
-%table.table.builds
- %thead
- %tr
- %th Status
- %th Build ID
- %th Stage
- %th Name
- %th Duration
- %th Finished at
- - if @project.coverage_enabled?
- %th Coverage
- %th
- = render @commit.builds_without_retry_sorted, controls: true
-
-- if @commit.retried_builds.any?
- %h3
- Retried builds
-
- %table.table.builds
- %thead
- %tr
- %th Status
- %th Build ID
- %th Stage
- %th Name
- %th Duration
- %th Finished at
- - if @project.coverage_enabled?
- %th Coverage
- %th
- = render @commit.retried_builds
diff --git a/app/views/ci/events/index.html.haml b/app/views/ci/events/index.html.haml
deleted file mode 100644
index 779f49b3d3a..00000000000
--- a/app/views/ci/events/index.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-%h3.page-title Events
-
-%table.table
- %thead
- %tr
- %th User ID
- %th Description
- %th When
- - @events.each do |event|
- %tr
- %td
- = event.user_id
- %td
- = event.description
- %td.light
- = time_ago_in_words event.updated_at
- ago
-
-= paginate @events \ No newline at end of file
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index e2179e60f3e..77f78caa8d8 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -4,29 +4,35 @@
syntax is correct
%i.fa.fa-ok.correct-syntax
- %table.table.table-bordered
- %thead
- %tr
- %th Parameter
- %th Value
- %tbody
- - @stages.each do |stage|
- - @builds.select { |build| build[:stage] == stage }.each do |build|
- %tr
- %td #{stage.capitalize} Job - #{build[:name]}
- %td
- %pre
- = simple_format build[:script]
+ .table-holder
+ %table.table.table-bordered
+ %thead
+ %tr
+ %th Parameter
+ %th Value
+ %tbody
+ - @stages.each do |stage|
+ - @builds.select { |build| build[:stage] == stage }.each do |build|
+ %tr
+ %td #{stage.capitalize} Job - #{build[:name]}
+ %td
+ %pre
+ = simple_format build[:commands]
- %br
- %b Tag list:
- = build[:tags]
- %br
- %b Refs only:
- = build[:only] && build[:only].join(", ")
- %br
- %b Refs except:
- = build[:except] && build[:except].join(", ")
+ %br
+ %b Tag list:
+ = build[:tags]
+ %br
+ %b Refs only:
+ = build[:only] && build[:only].join(", ")
+ %br
+ %b Refs except:
+ = build[:except] && build[:except].join(", ")
+ %br
+ %b When:
+ = build[:when]
+ - if build[:allow_failure]
+ %b Allowed to fail
-else
%p
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index a9b954771c5..fb9057e4882 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -11,15 +11,17 @@
.controls.pull-left.prepend-top-10
= submit_tag "Validate", class: 'btn btn-success submit-yml'
-
+
%p.text-center.loading
%i.fa.fa-refresh.fa-spin
.results.prepend-top-20
-:coffeescript
- $(".loading").hide()
- $('form').bind 'ajax:beforeSend', ->
- $(".loading").show()
- $('form').bind 'ajax:complete', ->
- $(".loading").hide()
+:javascript
+ $(".loading").hide();
+ $('form').bind('ajax:beforeSend', function() {
+ $(".loading").show();
+ });
+ $('form').bind('ajax:complete', function() {
+ $(".loading").hide();
+ });
diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/ci/notify/build_fail_email.html.haml
index d818e8b6756..de6291aa914 100644
--- a/app/views/ci/notify/build_fail_email.html.haml
+++ b/app/views/ci/notify/build_fail_email.html.haml
@@ -7,13 +7,17 @@
= @project.name
%p
- Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)}
+ Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.gl_project.namespace, @build.gl_project, @build.sha)}
%p
Author: #{@build.commit.git_author_name}
%p
- Branch: #{@build.commit.ref}
+ Branch: #{@build.ref}
+%p
+ Stage: #{@build.stage}
+%p
+ Job: #{@build.name}
%p
Message: #{@build.commit.git_commit_message}
%p
- Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)}
+ Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)}
diff --git a/app/views/ci/notify/build_fail_email.text.erb b/app/views/ci/notify/build_fail_email.text.erb
index 1add215a1c8..17a3b9b1d33 100644
--- a/app/views/ci/notify/build_fail_email.text.erb
+++ b/app/views/ci/notify/build_fail_email.text.erb
@@ -3,7 +3,9 @@ Build failed for <%= @project.name %>
Status: <%= @build.status %>
Commit: <%= @build.commit.short_sha %>
Author: <%= @build.commit.git_author_name %>
-Branch: <%= @build.commit.ref %>
+Branch: <%= @build.ref %>
+Stage: <%= @build.stage %>
+Job: <%= @build.name %>
Message: <%= @build.commit.git_commit_message %>
-Url: <%= ci_project_build_url(@build.project, @build) %>
+Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %>
diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/ci/notify/build_success_email.html.haml
index a20dcaee24e..6ef1fd67d89 100644
--- a/app/views/ci/notify/build_success_email.html.haml
+++ b/app/views/ci/notify/build_success_email.html.haml
@@ -8,13 +8,17 @@
= @project.name
%p
- Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)}
+ Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.gl_project.namespace, @build.gl_project, @build.sha)}
%p
Author: #{@build.commit.git_author_name}
%p
- Branch: #{@build.commit.ref}
+ Branch: #{@build.ref}
+%p
+ Stage: #{@build.stage}
+%p
+ Job: #{@build.name}
%p
Message: #{@build.commit.git_commit_message}
%p
- Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)}
+ Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)}
diff --git a/app/views/ci/notify/build_success_email.text.erb b/app/views/ci/notify/build_success_email.text.erb
index 7ebd17e7270..bc8b978c3d7 100644
--- a/app/views/ci/notify/build_success_email.text.erb
+++ b/app/views/ci/notify/build_success_email.text.erb
@@ -3,7 +3,9 @@ Build successful for <%= @project.name %>
Status: <%= @build.status %>
Commit: <%= @build.commit.short_sha %>
Author: <%= @build.commit.git_author_name %>
-Branch: <%= @build.commit.ref %>
+Branch: <%= @build.ref %>
+Stage: <%= @build.stage %>
+Job: <%= @build.name %>
Message: <%= @build.commit.git_commit_message %>
-Url: <%= ci_project_build_url(@build.project, @build) %>
+Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %>
diff --git a/app/views/ci/projects/_info.html.haml b/app/views/ci/projects/_info.html.haml
deleted file mode 100644
index 1888e1bde93..00000000000
--- a/app/views/ci/projects/_info.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- if no_runners_for_project?(@project)
- = render 'no_runners'
diff --git a/app/views/ci/projects/disabled.html.haml b/app/views/ci/projects/disabled.html.haml
deleted file mode 100644
index 83b0d8329e1..00000000000
--- a/app/views/ci/projects/disabled.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-Continuous Integration has been disabled for time of the migration.
diff --git a/app/views/ci/projects/edit.html.haml b/app/views/ci/projects/edit.html.haml
deleted file mode 100644
index 876ae5182d4..00000000000
--- a/app/views/ci/projects/edit.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- if @project.generated_yaml_config
- %p.alert.alert-danger
- CI Jobs are deprecated now, you can #{link_to "download", dumped_yaml_ci_project_path(@project)}
- or
- %a.preview-yml{:href => "#yaml-content", "data-toggle" => "modal"} preview
- yaml file which is based on your old jobs.
- Put this file to the root of your project and name it .gitlab-ci.yml
-
-= render 'form'
-
-- if @project.generated_yaml_config
- #yaml-content.modal.fade{"aria-hidden" => "true", "aria-labelledby" => ".gitlab-ci.yml", :role => "dialog", :tabindex => "-1"}
- .modal-dialog
- .modal-content
- .modal-header
- %button.close{"aria-hidden" => "true", "data-dismiss" => "modal", :type => "button"} ×
- %h4.modal-title Content of .gitlab-ci.yml
- .modal-body
- = text_area_tag :yaml, @project.generated_yaml_config, size: "70x25", class: "form-control"
- .modal-footer
- %button.btn.btn-default{"data-dismiss" => "modal", :type => "button"} Close
diff --git a/app/views/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml
new file mode 100644
index 00000000000..9c2290bc4a5
--- /dev/null
+++ b/app/views/ci/projects/index.html.haml
@@ -0,0 +1,20 @@
+.wiki
+ %h1
+ GitLab CI is now integrated in GitLab UI
+ %h2 For existing projects
+
+ %p
+ Check the following pages to find the CI status you're looking for:
+
+ %ul
+ %li Projects page - shows CI status for each project.
+ %li Project commits page - show CI status for each commit.
+
+
+
+ %h2 For new projects
+
+ %p
+ If you want to enable CI for a new project it is easy as adding
+ = link_to ".gitlab-ci.yml", "http://doc.gitlab.com/ce/ci/yaml/README.html"
+ file to your repository
diff --git a/app/views/ci/projects/show.html.haml b/app/views/ci/projects/show.html.haml
deleted file mode 100644
index 6443378af99..00000000000
--- a/app/views/ci/projects/show.html.haml
+++ /dev/null
@@ -1,60 +0,0 @@
-= render 'ci/shared/guide' unless @project.setup_finished?
-
-- if current_user && can?(current_user, :manage_project, gl_project) && !@project.any_runners?
- .alert.alert-danger
- Builds for this project wont be served unless you configure runners on
- = link_to "Runners page", ci_project_runners_path(@project)
-
-%ul.nav.nav-tabs.append-bottom-20
- %li{class: ref_tab_class}
- = link_to 'All commits', ci_project_path(@project)
- - @project.tracked_refs.each do |ref|
- %li{class: ref_tab_class(ref)}
- = link_to ref, ci_project_path(@project, ref: ref)
-
- - if @ref && !@project.tracked_refs.include?(@ref)
- %li{class: 'active'}
- = link_to @ref, ci_project_path(@project, ref: @ref)
-
- %li.pull-right
- = link_to 'View on GitLab', @project.gitlab_url, no_turbolink.merge( class: 'btn btn-sm' )
-
-- if @ref
- %p
- Paste build status image for #{@ref} with next link
- = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do
- Status Badge
- .badge-codes-block.bs-callout.bs-callout-info.hide
- %p
- Status badge for
- %span.label.label-info #{@ref}
- branch
- %div
- %label Markdown:
- = text_field_tag 'badge_md', markdown_badge_code(@project, @ref), readonly: true, class: 'form-control'
- %label Html:
- = text_field_tag 'badge_html', html_badge_code(@project, @ref), readonly: true, class: 'form-control'
-
-
-
-
-%table.table.builds
- %thead
- %tr
- %th Status
- %th Commit
- %th Message
- %th Branch
- %th Total duration
- %th Finished at
- - if @project.coverage_enabled?
- %th Coverage
-
- = render @commits
-
-= paginate @commits
-
-- if @commits.empty?
- .bs-callout
- %h4 No commits yet
-
diff --git a/app/views/ci/runners/show.html.haml b/app/views/ci/runners/show.html.haml
deleted file mode 100644
index ffec495f85a..00000000000
--- a/app/views/ci/runners/show.html.haml
+++ /dev/null
@@ -1,64 +0,0 @@
-= content_for :title do
- %h3.project-title
- Runner ##{@runner.id}
- .pull-right
- - if @runner.shared?
- %span.runner-state.runner-state-shared
- Shared
- - else
- %span.runner-state.runner-state-specific
- Specific
-
-%table.table
- %thead
- %tr
- %th Property Name
- %th Value
- %tr
- %td
- Tags
- %td
- - @runner.tag_list.each do |tag|
- %span.label.label-primary
- = tag
- %tr
- %td
- Name
- %td
- = @runner.name
- %tr
- %td
- Version
- %td
- = @runner.version
- %tr
- %td
- Revision
- %td
- = @runner.revision
- %tr
- %td
- Platform
- %td
- = @runner.platform
- %tr
- %td
- Architecture
- %td
- = @runner.architecture
- %tr
- %td
- Description
- %td
- = @runner.description
- %tr
- %td
- Last contact
- %td
- - if @runner.contacted_at
- #{time_ago_in_words(@runner.contacted_at)} ago
- - else
- Never
-
-
-
diff --git a/app/views/ci/services/edit.html.haml b/app/views/ci/services/edit.html.haml
deleted file mode 100644
index bcc5832792f..00000000000
--- a/app/views/ci/services/edit.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-= render 'form'
diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml
index 8a42f29b77c..db2d7f2f4b6 100644
--- a/app/views/ci/shared/_guide.html.haml
+++ b/app/views/ci/shared/_guide.html.haml
@@ -4,7 +4,7 @@
%ol
%li
Add at least one runner to the project.
- Go to #{link_to 'Runners page', ci_project_runners_path(@project), target: :blank} for instructions.
+ Go to #{link_to 'Runners page', runners_path(@project.gl_project), target: :blank} for instructions.
%li
Put the .gitlab-ci.yml in the root of your repository. Examples can be found in #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}.
You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
diff --git a/app/views/ci/user_sessions/new.html.haml b/app/views/ci/user_sessions/new.html.haml
index 308b217ea78..b8d9a1d7089 100644
--- a/app/views/ci/user_sessions/new.html.haml
+++ b/app/views/ci/user_sessions/new.html.haml
@@ -1,8 +1,7 @@
.login-block
%h2 Login using GitLab account
%p.light
- Make sure you have account on GitLab server
+ Make sure you have an account on the GitLab server
= link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink
%hr
= link_to "Login with GitLab", auth_ci_user_sessions_path(state: params[:state]), no_turbolink.merge( class: 'btn btn-login btn-success' )
-
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index 19d919f9b6a..f98fd9f06ba 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -3,10 +3,9 @@
.gray-content-block
- if current_user
- %ul.nav.nav-pills.event_filter.pull-right
- %li.pull-right
- = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'rss-btn' do
- %i.fa.fa-rss
+ .pull-right
+ = 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
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index ed480b8caf8..991e67b1cd3 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,3 +1,6 @@
+= content_for :flash_message do
+ = render 'shared/project_limit'
+
%ul.center-top-menu
= nav_link(path: ['projects#index', 'root#index']) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index c249f5cacec..d5b7e729e7b 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -8,12 +8,12 @@
= link_to new_group_path, class: "btn btn-new" do
%i.fa.fa-plus
New Group
- .title Welcome to the groups!
- Group members have access to all group projects.
+ .oneline
+ Group members have access to all group projects.
%ul.content-list
- @group_members.each do |group_member|
- group = group_member.group
= render 'shared/groups/group', group: group, group_member: group_member
-= paginate @group_members
+= paginate @group_members, theme: 'gitlab'
diff --git a/app/views/dashboard/milestones/_issue.html.haml b/app/views/dashboard/milestones/_issue.html.haml
index f689b9698eb..1408ebdd5dc 100644
--- a/app/views/dashboard/milestones/_issue.html.haml
+++ b/app/views/dashboard/milestones/_issue.html.haml
@@ -7,4 +7,4 @@
= link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title
.pull-right.assignee-icon
- if issue.assignee
- = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16"
+ = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16"
diff --git a/app/views/dashboard/milestones/_merge_request.html.haml b/app/views/dashboard/milestones/_merge_request.html.haml
index 8f5c4cce529..77c46de030b 100644
--- a/app/views/dashboard/milestones/_merge_request.html.haml
+++ b/app/views/dashboard/milestones/_merge_request.html.haml
@@ -7,4 +7,4 @@
= link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title
.pull-right.assignee-icon
- if merge_request.assignee
- = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16"
+ = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16"
diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml
index 55080d6b3fe..7c882a32702 100644
--- a/app/views/dashboard/milestones/_milestone.html.haml
+++ b/app/views/dashboard/milestones/_milestone.html.haml
@@ -16,7 +16,10 @@
= milestone_progress_bar(milestone)
.row
.col-sm-6
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label.label-gray
- = milestone.project.name_with_namespace
+ .expiration
+ = render 'shared/milestone_expired', milestone: milestone
+ .projects
+ - milestone.milestones.each do |milestone|
+ = link_to milestone_path(milestone) do
+ %span.label.label-gray
+ = milestone.project.name_with_namespace
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index 21b25c3986e..635251e2374 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -10,10 +10,10 @@
.milestones
%ul.content-list
- - if @dashboard_milestones.blank?
+ - if @milestones.blank?
%li
.nothing-here-block No milestones to show
- else
- - @dashboard_milestones.each do |milestone|
+ - @milestones.each do |milestone|
= render 'milestone', milestone: milestone
- = paginate @dashboard_milestones, theme: "gitlab"
+ = paginate @milestones, theme: "gitlab"
diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml
index 0d204ced7ea..3536bbeaf4b 100644
--- a/app/views/dashboard/milestones/show.html.haml
+++ b/app/views/dashboard/milestones/show.html.haml
@@ -1,85 +1,105 @@
-- page_title @dashboard_milestone.title, "Milestones"
-%h4.page-title
- .issue-box{ class: "issue-box-#{@dashboard_milestone.closed? ? 'closed' : 'open'}" }
- - if @dashboard_milestone.closed?
- Closed
- - else
- Open
- Milestone #{@dashboard_milestone.title}
+- page_title @milestone.title, "Milestones"
+- header_title "Milestones", dashboard_milestones_path
-%hr
-- if (@dashboard_milestone.total_items_count == @dashboard_milestone.closed_items_count) && @dashboard_milestone.active?
- .alert.alert-success
+.issuable-details
+ .page-title
+ .issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" }
+ - if @milestone.closed?
+ Closed
+ - else
+ Open
+ Milestone #{@milestone.title}
+
+ .gray-content-block.middle-block
+ %h2.issue-title
+ = gfm escape_once(@milestone.title)
+
+- 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.
-.description
-%table.table
- %thead
- %tr
- %th Project
- %th Open issues
- %th State
- %th Due date
- - @dashboard_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
+.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:
- #{@dashboard_milestone.closed_items_count} closed
+ #{@milestone.closed_items_count} closed
&ndash;
- #{@dashboard_milestone.open_items_count} open
- = milestone_progress_bar(@dashboard_milestone)
+ #{@milestone.open_items_count} open
+ = milestone_progress_bar(@milestone)
-%ul.nav.nav-tabs
+%ul.center-top-menu.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
- %span.badge= @dashboard_milestone.issue_count
+ %span.badge= @milestone.issue_count
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
Merge Requests
- %span.badge= @dashboard_milestone.merge_requests_count
+ %span.badge= @milestone.merge_requests_count
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
- %span.badge= @dashboard_milestone.participants.count
-
- .pull-right
- = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @dashboard_milestone.title), class: "btn edit-milestone-link btn-grouped"
+ %span.badge= @milestone.participants.count
.tab-content
.tab-pane.active#tab-issues
- .row
+ .gray-content-block.middle-block
+ .pull-right
+ = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
+
+ .oneline
+ All issues in this milestone
+
+ .row.prepend-top-default
.col-md-6
- = render 'issues', title: "Open", issues: @dashboard_milestone.opened_issues
+ = render 'issues', title: "Open", issues: @milestone.opened_issues
.col-md-6
- = render 'issues', title: "Closed", issues: @dashboard_milestone.closed_issues
+ = render 'issues', title: "Closed", issues: @milestone.closed_issues
.tab-pane#tab-merge-requests
- .row
+ .gray-content-block.middle-block
+ .pull-right
+ = link_to 'Browse Merge Requests', merge_requests_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
+
+ .oneline
+ All merge requests in this milestone
+
+ .row.prepend-top-default
.col-md-6
- = render 'merge_requests', title: "Open", merge_requests: @dashboard_milestone.opened_merge_requests
+ = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
.col-md-6
- = render 'merge_requests', title: "Closed", merge_requests: @dashboard_milestone.closed_merge_requests
+ = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
.tab-pane#tab-participants
+ .gray-content-block.middle-block
+ .oneline
+ All participants to this milestone
%ul.bordered-list
- - @dashboard_milestone.participants.each do |user|
+ - @milestone.participants.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
- = image_tag avatar_icon(user.email, 32), class: "avatar s32"
+ = 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/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml
index c8c315c73d8..81a5909e2d2 100644
--- a/app/views/dashboard/projects/_projects.html.haml
+++ b/app/views/dashboard/projects/_projects.html.haml
@@ -1,10 +1,11 @@
.projects-list-holder
.projects-search-form
.input-group
- = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control'
+ = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
- if current_user.can_create_project?
%span.input-group-btn
- = link_to new_project_path, class: 'btn btn-success' do
- New project
+ = link_to new_project_path, class: 'btn btn-green' do
+ %i.fa.fa-plus
+ New Project
= render 'shared/projects/list', projects: @projects, ci: true
diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder
index d2c51486841..c8c219f4cca 100644
--- a/app/views/dashboard/projects/index.atom.builder
+++ b/app/views/dashboard/projects/index.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
xml.id dashboard_projects_url
- xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 7a16b811f6b..53abf274bdb 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -3,7 +3,7 @@
= auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity")
- page_title "Projects"
-- header_title "Projects", root_path
+- header_title "Projects", dashboard_projects_path
= render 'dashboard/projects_head'
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 339362701d4..70705923d42 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -1,8 +1,11 @@
- page_title "Starred Projects"
-- header_title "Projects", projects_path
+- header_title "Projects", dashboard_projects_path
= render 'dashboard/projects_head'
+- if @last_push
+ = render "events/event_last_push", event: @last_push
+
- if @projects.any?
= render 'projects'
- else
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index d3908062f43..07b6d57932e 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -6,33 +6,29 @@
.gray-content-block
.pull-right
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
- Add new snippet
+ = icon('plus')
+ New Snippet
- .oneline
- Share code pastes with others out of git repository
-
-%ul.nav.nav-tabs.prepend-top-20
- = nav_tab :scope, nil do
- = link_to dashboard_snippets_path do
+ .btn-group.btn-group-next.snippet-scope-menu
+ = link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do
All
%span.badge
= current_user.snippets.count
- = nav_tab :scope, 'are_private' do
- = link_to dashboard_snippets_path(scope: 'are_private') do
+
+ = link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do
Private
%span.badge
= current_user.snippets.are_private.count
- = nav_tab :scope, 'are_internal' do
- = link_to dashboard_snippets_path(scope: 'are_internal') do
+
+ = link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do
Internal
%span.badge
= current_user.snippets.are_internal.count
- = nav_tab :scope, 'are_public' do
- = link_to dashboard_snippets_path(scope: 'are_public') do
+
+ = link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do
Public
%span.badge
= current_user.snippets.are_public.count
-.my-snippets
- = render 'snippets/snippets'
+= render 'snippets/snippets'
diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml
index 29ffe8a8be3..535e85869e5 100644
--- a/app/views/devise/passwords/new.html.haml
+++ b/app/views/devise/passwords/new.html.haml
@@ -6,7 +6,7 @@
.devise-errors
= devise_error_messages!
.clearfix.append-bottom-20
- = f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email]
+ = f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email], autofocus: true
.clearfix
= f.submit "Reset password", class: "btn-primary btn"
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index 3b0b19107ca..ba4c5b86efb 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -1,17 +1,19 @@
- page_title "Applications"
%h3.page-title Your applications
%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
-%table.table.table-striped
- %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
+
+.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
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index 80340aca54c..47442b78d48 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -2,26 +2,26 @@
%h3.page-title
Application: #{@application.name}
+.table-holder
+ %table.table
+ %tr
+ %td
+ Application Id
+ %td
+ %code#application_id= @application.uid
+ %tr
+ %td
+ Secret:
+ %td
+ %code#secret= @application.secret
-%table.table
- %tr
- %td
- Application Id
- %td
- %code#application_id= @application.uid
- %tr
- %td
- Secret:
- %td
- %code#secret= @application.secret
-
- %tr
- %td
- Callback url
- %td
- - @application.redirect_uri.split.each do |uri|
- %div
- %span.monospace= uri
+ %tr
+ %td
+ Callback url
+ %td
+ - @application.redirect_uri.split.each do |uri|
+ %div
+ %span.monospace= uri
.form-actions
= link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/doorkeeper/authorized_applications/index.html.haml b/app/views/doorkeeper/authorized_applications/index.html.haml
index 814cdc987ef..b184b9c01d4 100644
--- a/app/views/doorkeeper/authorized_applications/index.html.haml
+++ b/app/views/doorkeeper/authorized_applications/index.html.haml
@@ -1,16 +1,17 @@
%header.page-header
%h1 Your authorized applications
%main{:role => "main"}
- %table.table.table-striped
- %thead
- %tr
- %th Application
- %th Created At
- %th
- %th
- %tbody
- - @applications.each do |application|
+ .table-holder
+ %table.table.table-striped
+ %thead
%tr
- %td= application.name
- %td= application.created_at.strftime('%Y-%m-%d %H:%M:%S')
- %td= render 'delete_form', application: application \ No newline at end of file
+ %th Application
+ %th Created At
+ %th
+ %th
+ %tbody
+ - @applications.each do |application|
+ %tr
+ %td= application.name
+ %td= application.created_at.strftime('%Y-%m-%d %H:%M:%S')
+ %td= render 'delete_form', application: application
diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml
index 4259f64c191..fad65310021 100644
--- a/app/views/events/_event_issue.atom.haml
+++ b/app/views/events/_event_issue.atom.haml
@@ -1,3 +1,2 @@
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- - if issue.description.present?
- = markdown(issue.description, xhtml: true, reference_only_path: false, project: issue.project)
+ = markdown(issue.description, pipeline: :atom, project: issue.project)
diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml
index e8ed13df783..19bdc7b9ca5 100644
--- a/app/views/events/_event_merge_request.atom.haml
+++ b/app/views/events/_event_merge_request.atom.haml
@@ -1,3 +1,2 @@
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- - if merge_request.description.present?
- = markdown(merge_request.description, xhtml: true, reference_only_path: false, project: merge_request.project)
+ = markdown(merge_request.description, pipeline: :atom, project: merge_request.project)
diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml
index cfbfba50202..b730ebbd5f9 100644
--- a/app/views/events/_event_note.atom.haml
+++ b/app/views/events/_event_note.atom.haml
@@ -1,2 +1,2 @@
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- = markdown(note.note, xhtml: true, reference_only_path: false, project: note.project)
+ = markdown(note.note, pipeline: :atom, project: note.project)
diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml
index 3625cb49d8b..b271b9daff1 100644
--- a/app/views/events/_event_push.atom.haml
+++ b/app/views/events/_event_push.atom.haml
@@ -6,7 +6,7 @@
%i
at
= commit[:timestamp].to_time.to_s(:short)
- %blockquote= markdown(escape_once(commit[:message]), xhtml: true, reference_only_path: false, project: event.project)
+ %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project)
- if event.commits_count > 15
%p
%i
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 83d4d321c83..fcb07b04083 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -11,7 +11,7 @@
= form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f|
= hidden_field_tag :sort, @sort
.form-group
- = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search"
+ = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search", spellcheck: false
.form-group
= button_tag 'Search', class: "btn btn-default"
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 5a3d689d1e5..28b12c8dca8 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,9 +1,9 @@
.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"
+ = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search", spellcheck: false
.form-group
- = button_tag 'Search', class: "btn btn-success"
+ = button_tag 'Search', class: "btn"
.pull-right.hidden-sm.hidden-xs
- if current_user
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index 67e38ca3127..76bdd68fd76 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -1,5 +1,5 @@
- page_title "Projects"
-- header_title "Projects", root_path
+- header_title "Projects", dashboard_projects_path
- if current_user
= render 'dashboard/projects_head'
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index 596cb0a96cd..e30c3633223 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -1,5 +1,5 @@
- page_title "Projects"
-- header_title "Projects", root_path
+- header_title "Projects", dashboard_projects_path
- if current_user
= render 'dashboard/projects_head'
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index 5ea6d81c5b9..1412b19acde 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -1,5 +1,5 @@
- page_title "Projects"
-- header_title "Projects", root_path
+- header_title "Projects", dashboard_projects_path
- if current_user
= render 'dashboard/projects_head'
diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml
index 7e4fa7d4873..0f100c39ffb 100644
--- a/app/views/explore/snippets/index.html.haml
+++ b/app/views/explore/snippets/index.html.haml
@@ -10,7 +10,8 @@
- if current_user
.pull-right
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
- Add new snippet
+ = icon('plus')
+ New Snippet
.oneline
Public snippets created by you and other users are listed here
diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml
index 9ac56b1e5fe..11d69977ef9 100644
--- a/app/views/groups/_projects.html.haml
+++ b/app/views/groups/_projects.html.haml
@@ -1,10 +1,11 @@
.panel.panel-default.projects-list-holder
.panel-heading.clearfix
.input-group
- = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control'
+ = 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-success' do
- New project
+ = 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
+ = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false, skip_namespace: true
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index ae8fc9f85f0..8daac585960 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -3,8 +3,7 @@
.panel.panel-default
.panel-heading
- %strong= @group.name
- group settings:
+ Group settings
.panel-body
= form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f|
- if @group.errors.any?
@@ -25,6 +24,15 @@
%hr
= link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
+ .form-group
+ %hr
+ = f.label :public, class: 'control-label' do
+ Public
+ .col-sm-10
+ .checkbox
+ = f.check_box :public
+ %span.descr Make this group public (even if there is no any public project inside this group)
+
.form-actions
= f.submit 'Save group', class: "btn btn-save"
@@ -36,4 +44,5 @@
%br
%strong Removed group can not be restored!
- = link_to 'Remove Group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove"
+ .form-actions
+ = link_to 'Remove Group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove"
diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml
index b5f359279d5..a79a0fcdc8e 100644
--- a/app/views/groups/group_members/_group_member.html.haml
+++ b/app/views/groups/group_members/_group_member.html.haml
@@ -1,11 +1,10 @@
- user = member.user
- return unless user || member.invite?
-- show_roles = true if show_roles.nil?
%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
%span{class: ("list-item-name" if show_controls)}
- if member.user
- = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
+ = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
%strong
= link_to user.name, user_path(user)
%span.cgray= user.username
@@ -15,7 +14,7 @@
%label.label.label-danger
%strong Blocked
- else
- = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: ''
+ = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
%strong
= member.invite_email
%span.cgray
@@ -25,18 +24,19 @@
= link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at)
- - if show_controls && can?(current_user, :admin_group_member, member)
+ - if show_controls && can?(current_user, :admin_group_member, @group)
= link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
Resend invite
- - if show_roles
+ - if should_user_see_group_roles?(current_user, @group)
%span.pull-right
- %strong= member.human_access
+ %strong.member-access-level= member.human_access
- if show_controls
- if can?(current_user, :update_group_member, member)
= button_tag class: "btn-xs btn js-toggle-button",
title: 'Edit access level', type: 'button' do
%i.fa.fa-pencil-square-o
+
- if can?(current_user, :destroy_group_member, member)
&nbsp;
- if current_user == user
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 3a6d07ebddf..335bf036074 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,42 +1,38 @@
- page_title "Members"
- header_title group_title(@group, "Members", group_group_members_path(@group))
-- show_roles = should_user_see_group_roles?(current_user, @group)
-
-- if show_roles
- %p.light
- Members of group have access to all group projects.
- Read more about permissions
- %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
-
-
-.clearfix.js-toggle-container
- = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' }
- = button_tag 'Search', class: 'btn'
+- @blank_container = true
+.group-members-page
- if current_user && current_user.can?(:admin_group_member, @group)
- .pull-right
- = button_tag class: 'btn btn-new js-toggle-button', type: 'button' do
- Add members
- %i.fa.fa-chevron-down
-
- .js-toggle-content.hide.new-group-member-holder
- = render "new_group_member"
-
-.panel.panel-default.prepend-top-20
- .panel-heading
- %strong #{@group.name}
- group members
- %small
- (#{@members.total_count})
- %ul.well-list
- - @members.each do |member|
- = render 'groups/group_members/group_member', member: member, show_roles: show_roles, show_controls: true
-
-= paginate @members, theme: 'gitlab'
-
-:coffeescript
- $('form.member-search-form').on 'submit', (event) ->
- event.preventDefault()
- Turbolinks.visit @.action + '?' + $(@).serialize()
+ .panel.panel-default
+ .panel-heading
+ Add new user to group
+ .panel-body
+ - if should_user_see_group_roles?(current_user, @group)
+ %p.light
+ Members of group have access to all group projects.
+ .new-group-member-holder
+ = render "new_group_member"
+
+ .panel.panel-default
+ .panel-heading
+ %strong #{@group.name}
+ group members
+ %small
+ (#{@members.total_count})
+ .pull-right
+ = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
+ = button_tag class: 'btn', title: 'Search' do
+ = icon("search")
+ %ul.content-list
+ - @members.each do |member|
+ = render 'groups/group_members/group_member', member: member, show_controls: true
+ = paginate @members, theme: 'gitlab'
+
+:javascript
+ $('form.member-search-form').on('submit', function(event) {
+ event.preventDefault();
+ Turbolinks.visit(this.action + '?' + $(this).serialize());
+ });
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index 5bad48abafd..df726e2b2b9 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,2 +1,2 @@
:plain
- $("##{dom_id(@member)}").replaceWith('#{escape_javascript(render(@member, member: @member, show_controls: true))}');
+ $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member, show_controls: true))}');
diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml
index 09f9b4b8969..9b85d83d6d8 100644
--- a/app/views/groups/milestones/_issue.html.haml
+++ b/app/views/groups/milestones/_issue.html.haml
@@ -7,4 +7,4 @@
= link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title
.pull-right.assignee-icon
- if issue.assignee
- = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16", alt: ''
+ = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16", alt: ''
diff --git a/app/views/groups/milestones/_merge_request.html.haml b/app/views/groups/milestones/_merge_request.html.haml
index d0d1426762b..e3aa4aad198 100644
--- a/app/views/groups/milestones/_merge_request.html.haml
+++ b/app/views/groups/milestones/_merge_request.html.haml
@@ -7,4 +7,4 @@
= link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title
.pull-right.assignee-icon
- if merge_request.assignee
- = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16", alt: ''
+ = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: ''
diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml
index 41dffdd2fb8..a20bf75bc39 100644
--- a/app/views/groups/milestones/_milestone.html.haml
+++ b/app/views/groups/milestones/_milestone.html.haml
@@ -22,7 +22,7 @@
%span.label.label-gray
= milestone.project.name
.col-sm-6
- - if can?(current_user, :admin_group, @group)
+ - if can?(current_user, :admin_milestones, @group)
- if milestone.closed?
= link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
- else
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index 2bbcad5fdfb..84ec77c6188 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -3,15 +3,22 @@
= render 'shared/milestones_filter'
.gray-content-block
- Only milestones from
- %strong #{@group.name}
- group are listed here.
+ - 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
+ New Milestone
+
+ .oneline
+ Only milestones from
+ %strong #{@group.name}
+ group are listed here.
.milestones
%ul.content-list
- - if @group_milestones.blank?
+ - if @milestones.blank?
%li
.nothing-here-block No milestones to show
- else
- - @group_milestones.each do |milestone|
+ - @milestones.each do |milestone|
= render 'milestone', milestone: milestone
- = paginate @group_milestones, theme: "gitlab"
+ = paginate @milestones, theme: "gitlab"
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
new file mode 100644
index 00000000000..3894a0ece74
--- /dev/null
+++ b/app/views/groups/milestones/new.html.haml
@@ -0,0 +1,47 @@
+- page_title "Milestones"
+- header_title group_title(@group, "Milestones", group_milestones_path(@group))
+
+%h3.page-title
+ New Milestone
+
+%p.light
+ This will create milestone in every selected project
+%hr
+
+= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-requires-input' } do |f|
+ .row
+ .col-md-6
+ .form-group
+ = f.label :title, "Title", class: "control-label"
+ .col-sm-10
+ = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true
+ .form-group.milestone-description
+ = f.label :description, "Description", class: "control-label"
+ .col-sm-10
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
+ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
+ .clearfix
+ .error-alert
+ .form-group
+ = f.label :projects, "Projects", class: "control-label"
+ .col-sm-10
+ = f.collection_select :project_ids, @group.projects, :id, :name,
+ { selected: @group.projects.map(&:id) }, multiple: true, class: 'select2'
+
+ .col-md-6
+ .form-group
+ = f.label :due_date, "Due Date", class: "control-label"
+ .col-sm-10= f.hidden_field :due_date
+ .col-sm-10
+ .datepicker
+
+ .form-actions
+ = f.submit 'Create Milestone', class: "btn-create btn"
+ = link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel"
+
+
+:javascript
+ $(".datepicker").datepicker({
+ dateFormat: "yy-mm-dd",
+ onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
+ }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index 0c213f42186..3c1d8815013 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,93 +1,112 @@
-- page_title @group_milestone.title, "Milestones"
+- page_title @milestone.title, "Milestones"
= render "header_title"
-%h4.page-title
- .issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" }
- - if @group_milestone.closed?
- Closed
- - else
- Open
- Milestone #{@group_milestone.title}
- .pull-right
- - if can?(current_user, :admin_group, @group)
- - if @group_milestone.active?
- = link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close"
+.issuable-details
+ .page-title
+ .issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" }
+ - if @milestone.closed?
+ Closed
- else
- = link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
+ Open
+ 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"
+
+ .gray-content-block.middle-block
+ %h2.issue-title
+ = gfm escape_once(@milestone.title)
-%hr
-- if (@group_milestone.total_items_count == @group_milestone.closed_items_count) && @group_milestone.active?
- .alert.alert-success
+- 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.
-.description
-%table.table
- %thead
- %tr
- %th Project
- %th Open issues
- %th State
- %th Due date
- - @group_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
+.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:
- #{@group_milestone.closed_items_count} closed
+ #{@milestone.closed_items_count} closed
&ndash;
- #{@group_milestone.open_items_count} open
- = milestone_progress_bar(@group_milestone)
+ #{@milestone.open_items_count} open
+ = milestone_progress_bar(@milestone)
-%ul.nav.nav-tabs
+%ul.center-top-menu.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
- %span.badge= @group_milestone.issue_count
+ %span.badge= @milestone.issue_count
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
Merge Requests
- %span.badge= @group_milestone.merge_requests_count
+ %span.badge= @milestone.merge_requests_count
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
- %span.badge= @group_milestone.participants.count
-
- .pull-right
- = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @group_milestone.title), class: "btn edit-milestone-link btn-grouped"
+ %span.badge= @milestone.participants.count
.tab-content
.tab-pane.active#tab-issues
- .row
+ .gray-content-block.middle-block
+ .pull-right
+ = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped"
+
+ .oneline
+ All issues in this milestone
+
+ .row.prepend-top-default
.col-md-6
- = render 'issues', title: "Open", issues: @group_milestone.opened_issues
+ = render 'issues', title: "Open", issues: @milestone.opened_issues
.col-md-6
- = render 'issues', title: "Closed", issues: @group_milestone.closed_issues
+ = render 'issues', title: "Closed", issues: @milestone.closed_issues
.tab-pane#tab-merge-requests
- .row
+ .gray-content-block.middle-block
+ .pull-right
+ = link_to 'Browse Merge Requests', merge_requests_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped"
+
+ .oneline
+ All merge requests in this milestone
+
+ .row.prepend-top-default
.col-md-6
- = render 'merge_requests', title: "Open", merge_requests: @group_milestone.opened_merge_requests
+ = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
.col-md-6
- = render 'merge_requests', title: "Closed", merge_requests: @group_milestone.closed_merge_requests
+ = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
.tab-pane#tab-participants
+ .gray-content-block.middle-block
+ .oneline
+ All participants to this milestone
+
%ul.bordered-list
- - @group_milestone.participants.each do |user|
+ - @milestone.participants.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
- = image_tag avatar_icon(user.email, 32), class: "avatar s32"
+ = 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/groups/new.html.haml b/app/views/groups/new.html.haml
index 0665cdf387a..4bc31cabea6 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -1,5 +1,10 @@
- page_title 'New Group'
-- header_title 'New Group'
+- header_title "Groups", dashboard_groups_path
+
+%h3.page-title
+ New Group
+%hr
+
= form_for @group, html: { class: 'group-form form-horizontal' } do |f|
- if @group.errors.any?
.alert.alert-danger
@@ -18,3 +23,4 @@
.form-actions
= f.submit 'Create group', class: "btn btn-create", tabindex: 3
+ = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder
index a91d1a6e94b..7ea574434c3 100644
--- a/app/views/groups/show.atom.builder
+++ b/app/views/groups/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: group_url(@group), rel: "alternate", type: "text/html"
xml.id group_url(@group)
- xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index a9ba9d2ba10..dc8e81323a6 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -25,11 +25,9 @@
.hidden-xs
- if current_user
= render "events/event_last_push", event: @last_push
-
- %ul.nav.nav-pills.event_filter.pull-right
- %li
- = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'rss-btn' do
- %i.fa.fa-rss
+ .pull-right
+ = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' do
+ %i.fa.fa-rss
= render 'shared/event_filter'
%hr
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index e809d99ba71..7e801b5332d 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -102,6 +102,12 @@
%tr
%td.shortcut
.key g
+ .key b
+ %td
+ Go to builds
+ %tr
+ %td.shortcut
+ .key g
.key n
%td
Go to network graph
@@ -216,8 +222,8 @@
:javascript
- $('.js-more-help-button').click(function(e){
- $(this).remove()
- $('.hidden-shortcut').show()
- e.preventDefault()
+ $('.js-more-help-button').click(function (e) {
+ $(this).remove()l
+ $('.hidden-shortcut').show();
+ e.preventDefault();
});
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 7c89457ace3..2169a821fb2 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -15,6 +15,8 @@
%li
= link_to 'Tables', '#tables'
%li
+ = link_to 'Nav', '#nav'
+ %li
= link_to 'Buttons', '#buttons'
%li
= link_to 'Panels', '#panels'
@@ -30,17 +32,32 @@
%h2#blocks Blocks
%h3
- %code .well
+ %code .gray-content-block
+
- .well
- %h4 Something
+ .gray-content-block.middle-block
+ %h4 Normal block inside content
+ = lorem
+
+ .gray-content-block.second-block
+ %h4 Second block
= lorem
%h2#lists Lists
%h3
+ %code .content-list
+ %ul.content-list
+ %li
+ One item
+ %li
+ One item
+ %li
+ One item
+
+ %h3
%code .well-list
%ul.well-list
%li
@@ -102,11 +119,40 @@
%td the Bird
%td @twitter
+ %h2#navs Navigation
+
+ %h3
+ %code .center-top-menu
+ .example
+ %ul.center-top-menu
+ %li.active
+ %a Open
+ %li
+ %a Closed
+
+ %h3
+ %code .btn-group.btn-group-next
+ .example
+ %div.btn-group.btn-group-next
+ %a.btn.active Open
+ %a.btn Closed
+
+
+ %h3
+ %code .nav.nav-tabs
+ .example
+ %ul.nav.nav-tabs
+ %li.active
+ %a Open
+ %li
+ %a Closed
+
%h2#buttons Buttons
.example
%button.btn.btn-default{:type => "button"} Default
+ %button.btn.btn-gray{:type => "button"} Gray
%button.btn.btn-primary{:type => "button"} Primary
%button.btn.btn-success{:type => "button"} Success
%button.btn.btn-info{:type => "button"} Info
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 777eb482714..aec2e836c9f 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -1,4 +1,5 @@
- page_title "Bitbucket import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-bitbucket
Import projects from Bitbucket
@@ -14,45 +15,46 @@
= button_tag 'Import all projects', class: "btn btn-success js-import-all"
-%table.table.import-jobs
- %thead
- %tr
- %th From Bitbucket
- %th To GitLab
- %th Status
- %tbody
- - @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td
- = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank"
- %td
- %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
- %td.job-status
- - if project.import_status == 'finished'
- %span
- %i.fa.fa-check
- done
- - elsif project.import_status == 'started'
- %i.fa.fa-spinner.fa-spin
- started
- - else
- = project.human_import_status_name
+.table-holder
+ %table.table.import-jobs
+ %thead
+ %tr
+ %th From Bitbucket
+ %th To GitLab
+ %th Status
+ %tbody
+ - @already_added_projects.each do |project|
+ %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %td
+ = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank"
+ %td
+ %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ %td.job-status
+ - if project.import_status == 'finished'
+ %span
+ %i.fa.fa-check
+ done
+ - elsif project.import_status == 'started'
+ %i.fa.fa-spinner.fa-spin
+ started
+ - else
+ = project.human_import_status_name
- - @repos.each do |repo|
- %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
- %td
- = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
- %td.import-target
- = "#{repo["owner"]}/#{repo["slug"]}"
- %td.import-actions.job-status
- = button_tag "Import", class: "btn js-add-to-import"
- - @incompatible_repos.each do |repo|
- %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
- %td
- = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
- %td.import-target
- %td.import-actions-job-status
- = label_tag "Incompatible Project", nil, class: "label label-danger"
+ - @repos.each do |repo|
+ %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
+ %td
+ = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
+ %td.import-target
+ = "#{repo["owner"]}/#{repo["slug"]}"
+ %td.import-actions.job-status
+ = button_tag "Import", class: "btn js-add-to-import"
+ - @incompatible_repos.each do |repo|
+ %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
+ %td
+ = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
+ %td.import-target
+ %td.import-actions-job-status
+ = label_tag "Incompatible Project", nil, class: "label label-danger"
- if @incompatible_repos.any?
%p
@@ -65,5 +67,5 @@
again.
-:coffeescript
- new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}")
+:javascript
+ new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}");
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
index e1bb88ca4ed..5515fad6f48 100644
--- a/app/views/import/fogbugz/new.html.haml
+++ b/app/views/import/fogbugz/new.html.haml
@@ -1,4 +1,5 @@
- page_title "FogBugz Import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-bug
Import projects from FogBugz
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index 25cebfb3665..07338736bac 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -1,4 +1,5 @@
- page_title 'User map', 'FogBugz import'
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-bug
Import projects from FogBugz
@@ -22,28 +23,29 @@
%strong Map a FogBugz account ID to a GitLab user
%p
Selecting a GitLab user will add a link to the GitLab user in the descriptions
- of issues and comments (e.g. "By <a href="#">@johnsmith</a>"). It will also
+ of issues and comments (e.g. "By <a href="#">@johnsmith</a>"). It will also
associate and/or assign these issues and comments with the selected user.
- %table.table
- %thead
- %tr
- %th ID
- %th Name
- %th Email
- %th GitLab User
- %tbody
- - @user_map.each do |id, user|
+ .table-holder
+ %table.table
+ %thead
%tr
- %td= id
- %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
- %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
- %td
- = users_select_tag("users[#{id}][gitlab_user]", class: 'custom-form-control',
- scope: :all, email_user: true, selected: user[:gitlab_user])
+ %th ID
+ %th Name
+ %th Email
+ %th GitLab User
+ %tbody
+ - @user_map.each do |id, user|
+ %tr
+ %td= id
+ %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
+ %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
+ %td
+ = users_select_tag("users[#{id}][gitlab_user]", class: 'custom-form-control',
+ scope: :all, email_user: true, selected: user[:gitlab_user])
.form-actions
= submit_tag 'Continue to the next step', class: 'btn btn-create'
-:coffeescript
- new UsersSelect()
+:javascript
+ new UsersSelect();
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
index f179ece402d..6ee16c8be4b 100644
--- a/app/views/import/fogbugz/status.html.haml
+++ b/app/views/import/fogbugz/status.html.haml
@@ -1,4 +1,5 @@
- page_title "FogBugz import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-bug
Import projects from FogBugz
@@ -14,38 +15,39 @@
%p
= button_tag 'Import all projects', class: 'btn btn-success js-import-all'
-%table.table.import-jobs
- %thead
- %tr
- %th From FogBugz
- %th To GitLab
- %th Status
- %tbody
- - @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td
- = project.import_source
- %td
- %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
- %td.job-status
- - if project.import_status == 'finished'
- %span
- %i.fa.fa-check
- done
- - elsif project.import_status == 'started'
- %i.fa.fa-spinner.fa-spin
- started
- - else
- = project.human_import_status_name
+.table-holder
+ %table.table.import-jobs
+ %thead
+ %tr
+ %th From FogBugz
+ %th To GitLab
+ %th Status
+ %tbody
+ - @already_added_projects.each do |project|
+ %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %td
+ = project.import_source
+ %td
+ %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ %td.job-status
+ - if project.import_status == 'finished'
+ %span
+ %i.fa.fa-check
+ done
+ - elsif project.import_status == 'started'
+ %i.fa.fa-spinner.fa-spin
+ started
+ - else
+ = project.human_import_status_name
- - @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
- %td
- = repo.name
- %td.import-target
- = "#{current_user.username}/#{repo.name}"
- %td.import-actions.job-status
- = button_tag "Import", class: "btn js-add-to-import"
+ - @repos.each do |repo|
+ %tr{id: "repo_#{repo.id}"}
+ %td
+ = repo.name
+ %td.import-target
+ = "#{current_user.username}/#{repo.name}"
+ %td.import-actions.job-status
+ = button_tag "Import", class: "btn js-add-to-import"
-:coffeescript
- new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}")
+:javascript
+ new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}");
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index ef552498239..1416ee5bd5a 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -1,4 +1,5 @@
- page_title "GitHub import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-github
Import projects from GitHub
@@ -9,38 +10,39 @@
%p
= button_tag 'Import all projects', class: "btn btn-success js-import-all"
-%table.table.import-jobs
- %thead
- %tr
- %th From GitHub
- %th To GitLab
- %th Status
- %tbody
- - @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td
- = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank"
- %td
- %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
- %td.job-status
- - if project.import_status == 'finished'
- %span
- %i.fa.fa-check
- done
- - elsif project.import_status == 'started'
- %i.fa.fa-spinner.fa-spin
- started
- - else
- = project.human_import_status_name
+.table-holder
+ %table.table.import-jobs
+ %thead
+ %tr
+ %th From GitHub
+ %th To GitLab
+ %th Status
+ %tbody
+ - @already_added_projects.each do |project|
+ %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %td
+ = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank"
+ %td
+ %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ %td.job-status
+ - if project.import_status == 'finished'
+ %span
+ %i.fa.fa-check
+ done
+ - elsif project.import_status == 'started'
+ %i.fa.fa-spinner.fa-spin
+ started
+ - else
+ = project.human_import_status_name
- - @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
- %td
- = link_to repo.full_name, "https://github.com/#{repo.full_name}", target: "_blank"
- %td.import-target
- = repo.full_name
- %td.import-actions.job-status
- = button_tag "Import", class: "btn js-add-to-import"
+ - @repos.each do |repo|
+ %tr{id: "repo_#{repo.id}"}
+ %td
+ = link_to repo.full_name, "https://github.com/#{repo.full_name}", target: "_blank"
+ %td.import-target
+ = repo.full_name
+ %td.import-actions.job-status
+ = button_tag "Import", class: "btn js-add-to-import"
-:coffeescript
- new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}")
+:javascript
+ new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}");
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index 727f3c7e7fa..911a55eb85d 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -1,4 +1,5 @@
- page_title "GitLab.com import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-heart
Import projects from GitLab.com
@@ -9,38 +10,39 @@
%p
= button_tag 'Import all projects', class: "btn btn-success js-import-all"
-%table.table.import-jobs
- %thead
- %tr
- %th From GitLab.com
- %th To this GitLab instance
- %th Status
- %tbody
- - @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td
- = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank"
- %td
- %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
- %td.job-status
- - if project.import_status == 'finished'
- %span
- %i.fa.fa-check
- done
- - elsif project.import_status == 'started'
- %i.fa.fa-spinner.fa-spin
- started
- - else
- = project.human_import_status_name
+.table-holder
+ %table.table.import-jobs
+ %thead
+ %tr
+ %th From GitLab.com
+ %th To this GitLab instance
+ %th Status
+ %tbody
+ - @already_added_projects.each do |project|
+ %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %td
+ = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank"
+ %td
+ %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ %td.job-status
+ - if project.import_status == 'finished'
+ %span
+ %i.fa.fa-check
+ done
+ - elsif project.import_status == 'started'
+ %i.fa.fa-spinner.fa-spin
+ started
+ - else
+ = project.human_import_status_name
- - @repos.each do |repo|
- %tr{id: "repo_#{repo["id"]}"}
- %td
- = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
- %td.import-target
- = repo["path_with_namespace"]
- %td.import-actions.job-status
- = button_tag "Import", class: "btn js-add-to-import"
+ - @repos.each do |repo|
+ %tr{id: "repo_#{repo["id"]}"}
+ %td
+ = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
+ %td.import-target
+ = repo["path_with_namespace"]
+ %td.import-actions.job-status
+ = button_tag "Import", class: "btn js-add-to-import"
-:coffeescript
- new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}")
+:javascript
+ new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}");
diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml
index bff7ee7c85d..6b0fa1edf8c 100644
--- a/app/views/import/gitorious/status.html.haml
+++ b/app/views/import/gitorious/status.html.haml
@@ -1,4 +1,5 @@
- page_title "Gitorious import"
+- header_title "Projects", root_path
%h3.page-title
%i.icon-gitorious.icon-gitorious-big
Import projects from Gitorious.org
@@ -9,38 +10,39 @@
%p
= button_tag 'Import all projects', class: "btn btn-success js-import-all"
-%table.table.import-jobs
- %thead
- %tr
- %th From Gitorious.org
- %th To GitLab
- %th Status
- %tbody
- - @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td
- = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank"
- %td
- %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
- %td.job-status
- - if project.import_status == 'finished'
- %span
- %i.fa.fa-check
- done
- - elsif project.import_status == 'started'
- %i.fa.fa-spinner.fa-spin
- started
- - else
- = project.human_import_status_name
+.table-holder
+ %table.table.import-jobs
+ %thead
+ %tr
+ %th From Gitorious.org
+ %th To GitLab
+ %th Status
+ %tbody
+ - @already_added_projects.each do |project|
+ %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %td
+ = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank"
+ %td
+ %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ %td.job-status
+ - if project.import_status == 'finished'
+ %span
+ %i.fa.fa-check
+ done
+ - elsif project.import_status == 'started'
+ %i.fa.fa-spinner.fa-spin
+ started
+ - else
+ = project.human_import_status_name
- - @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
- %td
- = link_to repo.full_name, "https://gitorious.org/#{repo.full_name}", target: "_blank"
- %td.import-target
- = repo.full_name
- %td.import-actions.job-status
- = button_tag "Import", class: "btn js-add-to-import"
+ - @repos.each do |repo|
+ %tr{id: "repo_#{repo.id}"}
+ %td
+ = link_to repo.full_name, "https://gitorious.org/#{repo.full_name}", target: "_blank"
+ %td.import-target
+ = repo.full_name
+ %td.import-actions.job-status
+ = button_tag "Import", class: "btn js-add-to-import"
-:coffeescript
- new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}")
+:javascript
+ new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}");
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index 9c64e0a009f..5d2f149cd5f 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -1,4 +1,5 @@
- page_title "Google Code import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-google
Import projects from Google Code
@@ -6,7 +7,7 @@
= form_tag callback_import_google_code_path, class: 'form-horizontal', multipart: true do
%p
- Follow the steps below to export your Google Code project data.
+ Follow the steps below to export your Google Code project data.
In the next step, you'll be able to select the projects you want to import.
%ol
%li
diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml
index e53ebda7dc1..0738b3db1eb 100644
--- a/app/views/import/google_code/new_user_map.html.haml
+++ b/app/views/import/google_code/new_user_map.html.haml
@@ -1,4 +1,5 @@
- page_title "User map", "Google Code import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-google
Import projects from Google Code
@@ -8,31 +9,31 @@
%p
Customize how Google Code email addresses and usernames are imported into GitLab.
In the next step, you'll be able to select the projects you want to import.
- %p
+ %p
The user map is a JSON document mapping the Google Code users that participated on your projects to the way their email addresses and usernames will be imported into GitLab. You can change this by changing the value on the right hand side of <code>:</code>. Be sure to preserve the surrounding double quotes, other punctuation and the email address or username on the left hand side.
%ul
%li
%strong Default: Directly import the Google Code email address or username
%p
- <code>"johnsmith@example.com": "johnsm...@example.com"</code>
- will add "By johnsm...@example.com" to all issues and comments originally created by johnsmith@example.com.
+ <code>"johnsmith@example.com": "johnsm...@example.com"</code>
+ will add "By johnsm...@example.com" to all issues and comments originally created by johnsmith@example.com.
The email address or username is masked to ensure the user's privacy.
%li
%strong Map a Google Code user to a GitLab user
%p
- <code>"johnsmith@example.com": "@johnsmith"</code>
- will add "By <a href="#">@johnsmith</a>" to all issues and comments originally created by johnsmith@example.com,
+ <code>"johnsmith@example.com": "@johnsmith"</code>
+ will add "By <a href="#">@johnsmith</a>" to all issues and comments originally created by johnsmith@example.com,
and will set <a href="#">@johnsmith</a> as the assignee on all issues originally assigned to johnsmith@example.com.
%li
%strong Map a Google Code user to a full name
%p
- <code>"johnsmith@example.com": "John Smith"</code>
+ <code>"johnsmith@example.com": "John Smith"</code>
will add "By John Smith" to all issues and comments originally created by johnsmith@example.com.
%li
%strong Map a Google Code user to a full email address
%p
- <code>"johnsmith@example.com": "johnsmith@example.com"</code>
- will add "By <a href="#">johnsmith@example.com</a>" to all issues and comments originally created by johnsmith@example.com.
+ <code>"johnsmith@example.com": "johnsmith@example.com"</code>
+ will add "By <a href="#">johnsmith@example.com</a>" to all issues and comments originally created by johnsmith@example.com.
By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address.
.form-group
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index e8ec79e72f7..175ef6921cd 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -1,4 +1,5 @@
- page_title "Google Code import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-google
Import projects from Google Code
@@ -17,45 +18,46 @@
- else
= button_tag 'Import all projects', class: "btn btn-success js-import-all"
-%table.table.import-jobs
- %thead
- %tr
- %th From Google Code
- %th To GitLab
- %th Status
- %tbody
- - @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td
- = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank"
- %td
- %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
- %td.job-status
- - if project.import_status == 'finished'
- %span
- %i.fa.fa-check
- done
- - elsif project.import_status == 'started'
- %i.fa.fa-spinner.fa-spin
- started
- - else
- = project.human_import_status_name
+.table-holder
+ %table.table.import-jobs
+ %thead
+ %tr
+ %th From Google Code
+ %th To GitLab
+ %th Status
+ %tbody
+ - @already_added_projects.each do |project|
+ %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %td
+ = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank"
+ %td
+ %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ %td.job-status
+ - if project.import_status == 'finished'
+ %span
+ %i.fa.fa-check
+ done
+ - elsif project.import_status == 'started'
+ %i.fa.fa-spinner.fa-spin
+ started
+ - else
+ = project.human_import_status_name
- - @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
- %td
- = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
- %td.import-target
- = "#{current_user.username}/#{repo.name}"
- %td.import-actions.job-status
- = button_tag "Import", class: "btn js-add-to-import"
- - @incompatible_repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
- %td
- = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
- %td.import-target
- %td.import-actions-job-status
- = label_tag "Incompatible Project", nil, class: "label label-danger"
+ - @repos.each do |repo|
+ %tr{id: "repo_#{repo.id}"}
+ %td
+ = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+ %td.import-target
+ = "#{current_user.username}/#{repo.name}"
+ %td.import-actions.job-status
+ = button_tag "Import", class: "btn js-add-to-import"
+ - @incompatible_repos.each do |repo|
+ %tr{id: "repo_#{repo.id}"}
+ %td
+ = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+ %td.import-target
+ %td.import-actions-job-status
+ = label_tag "Incompatible Project", nil, class: "label label-danger"
- if @incompatible_repos.any?
%p
@@ -66,5 +68,5 @@
= link_to "import flow", new_import_google_code_path
again.
-:coffeescript
- new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}")
+:javascript
+ new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}");
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index c3b137e3ddf..74174a72f5a 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -3,7 +3,7 @@
%meta{charset: "utf-8"}
%meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
%meta{content: "GitLab Community Edition", name: "description"}
- %meta{name: 'referrer', content: 'origin'}
+ %meta{name: 'referrer', content: 'origin-when-cross-origin'}
%title= page_title
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 3c58f10e759..035fe0056d3 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -1,3 +1,4 @@
+- 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.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 2468687b56d..ec7cd79bc54 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,12 +1,12 @@
-.page-with-sidebar{ class: nav_sidebar_class }
+.page-with-sidebar{ class: page_sidebar_class }
= render "layouts/broadcast"
- .sidebar-wrapper.nicescroll
+ .sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do
= brand_header_logo
.gitlab-text-container
%h3 GitLab
-
+
- if defined?(sidebar) && sidebar
= render "layouts/nav/#{sidebar}"
- elsif current_user
@@ -17,12 +17,13 @@
.collapse-nav
= render partial: 'layouts/collapse_button'
- if current_user
- = link_to current_user, class: 'sidebar-user' do
- = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s36'
+ = link_to current_user, class: 'sidebar-user', title: "Profile" do
+ = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36'
.username
= current_user.username
.content-wrapper
= render "layouts/flash"
+ = yield :flash_message
%div{ class: container_class }
.content
.clearfix
diff --git a/app/views/layouts/_piwik.html.haml b/app/views/layouts/_piwik.html.haml
index 135e8daca26..259b4f7cdfc 100644
--- a/app/views/layouts/_piwik.html.haml
+++ b/app/views/layouts/_piwik.html.haml
@@ -1,12 +1,14 @@
+<!-- Piwik -->
:javascript
var _paq = _paq || [];
- _paq.push(["trackPageView"]);
- _paq.push(["enableLinkTracking"]);
-
+ _paq.push(['trackPageView']);
+ _paq.push(['enableLinkTracking']);
(function() {
- var u=(("https:" == document.location.protocol) ? "https" : "http") + "://#{extra_config.piwik_url}/";
- _paq.push(["setTrackerUrl", u+"piwik.php"]);
- _paq.push(["setSiteId", "#{extra_config.piwik_site_id}"]);
- var d=document, g=d.createElement("script"), s=d.getElementsByTagName("script")[0]; g.type="text/javascript";
- g.defer=true; g.async=true; g.src=u+"piwik.js"; s.parentNode.insertBefore(g,s);
+ var u="//#{extra_config.piwik_url}/";
+ _paq.push(['setTrackerUrl', u+'piwik.php']);
+ _paq.push(['setSiteId', #{extra_config.piwik_site_id}]);
+ var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
+ g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
+<noscript><p><img src="//#{extra_config.piwik_url}/piwik.php?idsite=#{extra_config.piwik_site_id}" style="border:0;" alt="" /></p></noscript>
+<!-- End Piwik Code -->
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index e2d2dec7ab8..a44f5762a6b 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"
+ = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input form-control", spellcheck: false
= hidden_field_tag :group_id, @group.try(:id)
- if @project && @project.persisted?
= hidden_field_tag :project_id, @project.id
@@ -11,6 +11,8 @@
= hidden_field_tag :scope, 'merge_requests'
- elsif current_controller?(:wikis)
= hidden_field_tag :scope, 'wiki_blobs'
+ - elsif current_controller?(:commits)
+ = hidden_field_tag :scope, 'commits'
- else
= hidden_field_tag :search_code, true
@@ -23,6 +25,6 @@
:javascript
$('.search-input').on('keyup', function(e) {
if (e.keyCode == 27) {
- $('.search-input').blur()
+ $('.search-input').blur();
}
- })
+ });
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index 1c738719bd8..6591c52bdbd 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -1,5 +1,5 @@
-- page_title "Admin area"
-- header_title "Admin area", admin_root_path
+- page_title "Admin Area"
+- header_title "Admin Area", admin_root_path
- sidebar "admin"
= render template: "layouts/application"
diff --git a/app/views/layouts/ci/_nav_admin.html.haml b/app/views/layouts/ci/_nav_admin.html.haml
index af2545a22d8..dcda04a4638 100644
--- a/app/views/layouts/ci/_nav_admin.html.haml
+++ b/app/views/layouts/ci/_nav_admin.html.haml
@@ -9,23 +9,25 @@
= nav_link path: 'projects#index' do
= link_to ci_admin_projects_path do
= icon('list-alt fw')
- Projects
+ %span
+ Projects
= nav_link path: 'events#index' do
= link_to ci_admin_events_path do
= icon('book fw')
- Events
+ %span
+ Events
= nav_link path: ['runners#index', 'runners#show'] do
= link_to ci_admin_runners_path do
= icon('cog fw')
- Runners
- %small.pull-right
- = Ci::Runner.count(:all)
+ %span
+ Runners
+ %span.count= Ci::Runner.count(:all)
= nav_link path: 'builds#index' do
= link_to ci_admin_builds_path do
= icon('link fw')
- Builds
- %small.pull-right
- = Ci::Build.count(:all)
+ %span
+ Builds
+ %span.count= Ci::Build.count(:all)
= nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
= link_to ci_admin_application_settings_path do
= icon('cogs fw')
diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml
index 7daf9342e42..f094edbfa87 100644
--- a/app/views/layouts/ci/_nav_project.html.haml
+++ b/app/views/layouts/ci/_nav_project.html.haml
@@ -5,45 +5,8 @@
%span
Back to project
%li.separate-item
- = nav_link path: ['projects#show', 'commits#show', 'builds#show'] do
- = link_to ci_project_path(@project) do
- = icon('list-alt fw')
- %span
- Commits
- %span.count= @project.commits.count
- = nav_link path: ['runners#index', 'runners#show', 'runners#edit'] do
- = link_to ci_project_runners_path(@project) do
- = icon('cog fw')
- %span
- Runners
- = nav_link path: 'variables#show' do
- = link_to ci_project_variables_path(@project) do
- = icon('code fw')
- %span
- Variables
- = nav_link path: 'web_hooks#index' do
- = link_to ci_project_web_hooks_path(@project) do
- = icon('link fw')
- %span
- Web Hooks
- = nav_link path: 'triggers#index' do
- = link_to ci_project_triggers_path(@project) do
- = icon('retweet fw')
- %span
- Triggers
- = nav_link path: ['services#index', 'services#edit'] do
- = link_to ci_project_services_path(@project) do
- = icon('share fw')
- %span
- Services
= nav_link path: 'events#index' do
= link_to ci_project_events_path(@project) do
= icon('book fw')
%span
Events
- %li.separate-item
- = nav_link path: 'projects#edit' do
- = link_to edit_ci_project_path(@project) do
- = icon('cogs fw')
- %span
- Settings
diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml
index bb5ec727bff..7e90af21b21 100644
--- a/app/views/layouts/ci/_page.html.haml
+++ b/app/views/layouts/ci/_page.html.haml
@@ -1,8 +1,8 @@
-.page-with-sidebar{ class: nav_sidebar_class }
+.page-with-sidebar{ class: page_sidebar_class }
= render "layouts/broadcast"
- .sidebar-wrapper.nicescroll
+ .sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do
= brand_header_logo
.gitlab-text-container
%h3 GitLab
@@ -14,8 +14,8 @@
.collapse-nav
= render partial: 'layouts/collapse_button'
- if current_user
- = link_to current_user, class: 'sidebar-user' do
- = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s36'
+ = link_to current_user, class: 'sidebar-user', title: "Profile" do
+ = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36'
.username
= current_user.username
.content-wrapper
diff --git a/app/views/layouts/ci/build.html.haml b/app/views/layouts/ci/build.html.haml
deleted file mode 100644
index a1356f0dc2e..00000000000
--- a/app/views/layouts/ci/build.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-!!! 5
-%html{ lang: "en"}
- = render 'layouts/head'
- %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
- - header_title ci_commit_title(@commit)
- - if current_user
- = render "layouts/header/default", title: header_title
- - else
- = render "layouts/header/public", title: header_title
-
- = render 'layouts/ci/page', sidebar: 'nav_project'
diff --git a/app/views/layouts/ci/commit.html.haml b/app/views/layouts/ci/commit.html.haml
deleted file mode 100644
index a1356f0dc2e..00000000000
--- a/app/views/layouts/ci/commit.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-!!! 5
-%html{ lang: "en"}
- = render 'layouts/head'
- %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
- - header_title ci_commit_title(@commit)
- - if current_user
- = render "layouts/header/default", title: header_title
- - else
- = render "layouts/header/public", title: header_title
-
- = render 'layouts/ci/page', sidebar: 'nav_project'
diff --git a/app/views/layouts/ci/project.html.haml b/app/views/layouts/ci/project.html.haml
deleted file mode 100644
index 15478c3f5bc..00000000000
--- a/app/views/layouts/ci/project.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-!!! 5
-%html{ lang: "en"}
- = render 'layouts/head'
- %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
- - header_title @project.name, ci_project_path(@project)
- - if current_user
- = render "layouts/header/default", title: header_title
- - else
- = render "layouts/header/public", title: header_title
-
- = render 'layouts/ci/page', sidebar: 'nav_project'
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 95e077c339f..f08cb0a5428 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,13 +1,13 @@
!!! 5
%html{ lang: "en"}
= render "layouts/head"
- %body.ui_charcoal.login-page.application
+ %body.ui_charcoal.login-page.application.navless
= render "layouts/header/empty"
= render "layouts/broadcast"
.container.navless-container
.content
= render "layouts/flash"
- .row.prepend-top-20
+ .row
.col-sm-5.pull-right
= yield
.col-sm-7.brand-holder.pull-left
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index 2af265a2296..915acc4612e 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: "en"}
= render "layouts/head"
- %body{class: "#{user_application_theme} application"}
+ %body{class: "#{user_application_theme} application navless"}
= render "layouts/header/empty"
.container.navless-container
= render "layouts/flash"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index c31b1cbe9a8..3892ef8eefa 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -11,18 +11,27 @@
%li.hidden-sm.hidden-xs
= render 'layouts/search'
%li.visible-sm.visible-xs
- = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
+ - if session[:impersonator_id]
+ %li.impersonation
+ = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = icon('user-secret fw')
- if current_user.is_admin?
%li
- = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw')
- if current_user.can_create_project?
%li
- = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('plus fw')
+ - if Gitlab::Sherlock.enabled?
+ %li
+ = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
+ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('tachometer fw')
%li
- = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('sign-out')
%h1.title= title
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 2079feeeab6..d04a3d1f227 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -5,78 +5,78 @@
%span
Overview
= nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects', data: {placement: 'right'} do
+ = link_to admin_namespaces_projects_path, title: 'Projects' do
= icon('cube fw')
%span
Projects
= nav_link(controller: :users) do
- = link_to admin_users_path, title: 'Users', data: {placement: 'right'} do
+ = link_to admin_users_path, title: 'Users' do
= icon('user fw')
%span
Users
= nav_link(controller: :groups) do
- = link_to admin_groups_path, title: 'Groups', data: {placement: 'right'} do
+ = link_to admin_groups_path, title: 'Groups' do
= icon('group fw')
%span
Groups
= nav_link(controller: :deploy_keys) do
- = link_to admin_deploy_keys_path, title: 'Deploy Keys', data: {placement: 'right'} do
+ = link_to admin_deploy_keys_path, title: 'Deploy Keys' do
= icon('key fw')
%span
Deploy Keys
= nav_link do
- = link_to ci_admin_projects_path, title: 'Continuous Integration', data: {placement: 'right'} do
+ = link_to ci_admin_projects_path, title: 'Continuous Integration' do
= icon('building fw')
%span
Continuous Integration
= nav_link(controller: :logs) do
- = link_to admin_logs_path, title: 'Logs', data: {placement: 'right'} do
+ = link_to admin_logs_path, title: 'Logs' do
= icon('file-text fw')
%span
Logs
= nav_link(controller: :broadcast_messages) do
- = link_to admin_broadcast_messages_path, title: 'Broadcast Messages', data: {placement: 'right'} do
+ = link_to admin_broadcast_messages_path, title: 'Messages' do
= icon('bullhorn fw')
%span
Messages
= nav_link(controller: :hooks) do
- = link_to admin_hooks_path, title: 'Hooks', data: {placement: 'right'} do
+ = link_to admin_hooks_path, title: 'Hooks' do
= icon('external-link fw')
%span
Hooks
= nav_link(controller: :background_jobs) do
- = link_to admin_background_jobs_path, title: 'Background Jobs', data: {placement: 'right'} do
+ = link_to admin_background_jobs_path, title: 'Background Jobs' do
= icon('cog fw')
%span
Background Jobs
= nav_link(controller: :applications) do
- = link_to admin_applications_path, title: 'Applications', data: {placement: 'right'} do
+ = link_to admin_applications_path, title: 'Applications' do
= icon('cloud fw')
%span
Applications
= nav_link(controller: :services) do
- = link_to admin_application_settings_services_path, title: 'Service Templates', data: {placement: 'right'} do
+ = link_to admin_application_settings_services_path, title: 'Service Templates' do
= icon('copy fw')
%span
Service Templates
= nav_link(controller: :labels) do
- = link_to admin_labels_path, title: 'Labels', data: {placement: 'right'} do
+ = link_to admin_labels_path, title: 'Labels' do
= icon('tags fw')
%span
Labels
= nav_link(controller: :abuse_reports) do
- = link_to admin_abuse_reports_path, title: "Abuse reports" do
+ = link_to admin_abuse_reports_path, title: "Abuse Reports" do
= icon('exclamation-circle fw')
%span
Abuse Reports
%span.count= AbuseReport.count(:all)
= nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
- = link_to admin_application_settings_path, title: 'Settings', data: {placement: 'right'} do
+ = link_to admin_application_settings_path, title: 'Settings' do
= icon('cogs fw')
%span
Settings
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 21d8655131f..da698831300 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,50 +1,50 @@
%ul.nav.nav-sidebar
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: 'home'}) do
- = link_to root_path, title: 'Projects', data: {placement: 'right'} do
+ = link_to dashboard_projects_path, title: 'Projects' do
= icon('home fw')
%span
Projects
= nav_link(path: 'dashboard#activity') do
- = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity', data: {placement: 'right'} do
+ = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do
= icon('dashboard fw')
%span
Activity
= nav_link(controller: :groups) do
- = link_to dashboard_groups_path, title: 'Groups', data: {placement: 'right'} do
+ = link_to dashboard_groups_path, title: 'Groups' do
= icon('group fw')
%span
Groups
= nav_link(controller: :milestones) do
- = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do
+ = link_to dashboard_milestones_path, title: 'Milestones' do
= icon('clock-o fw')
%span
Milestones
= nav_link(path: 'dashboard#issues') do
- = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do
+ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do
= icon('exclamation-circle fw')
%span
Issues
%span.count= current_user.assigned_issues.opened.count
= nav_link(path: 'dashboard#merge_requests') do
- = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do
+ = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do
= icon('tasks fw')
%span
Merge Requests
%span.count= current_user.assigned_merge_requests.opened.count
= nav_link(controller: :snippets) do
- = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
+ = link_to dashboard_snippets_path, title: 'Snippets' do
= icon('clipboard fw')
%span
Snippets
= nav_link(controller: :help) do
- = link_to help_path, title: 'Help', data: {placement: 'right'} do
+ = link_to help_path, title: 'Help' do
= icon('question-circle fw')
%span
Help
%li.separate-item
= nav_link(controller: :profile) do
- = link_to profile_path, title: 'Profile settings', data: {placement: 'bottom'} do
+ = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
= icon('user fw')
%span
Profile Settings
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index 21e565972a7..48039ca2918 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -1,21 +1,21 @@
%ul.nav.nav-sidebar
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
- = link_to explore_root_path, title: 'Projects', data: {placement: 'right'} do
+ = link_to explore_root_path, title: 'Projects' do
= icon('home fw')
%span
Projects
= nav_link(controller: :groups) do
- = link_to explore_groups_path, title: 'Groups', data: {placement: 'right'} do
+ = link_to explore_groups_path, title: 'Groups' do
= icon('group fw')
%span
Groups
= nav_link(controller: :snippets) do
- = link_to explore_snippets_path, title: 'Snippets', data: {placement: 'right'} do
+ = link_to explore_snippets_path, title: 'Snippets' do
= icon('clipboard fw')
%span
Snippets
= nav_link(controller: :help) do
- = link_to help_path, title: 'Help', data: {placement: 'right'} do
+ = link_to help_path, title: 'Help' do
= icon('question-circle fw')
%span
Help
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index eb35af22b93..68da8d5de2a 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -1,46 +1,46 @@
%ul.nav.nav-sidebar
= nav_link do
- = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do
+ = link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to dashboard
+ Go to dashboard
%li.separate-item
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
- = link_to group_path(@group), title: 'Home', data: {placement: 'right'} do
+ = link_to group_path(@group), title: 'Home' do
= icon('dashboard fw')
%span
Group
- if can?(current_user, :read_group, @group)
- if current_user
= nav_link(controller: [:group, :milestones]) do
- = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} do
+ = link_to group_milestones_path(@group), title: 'Milestones' do
= icon('clock-o fw')
%span
Milestones
= nav_link(path: 'groups#issues') do
- = link_to issues_group_path(@group), title: 'Issues', data: {placement: 'right'} do
+ = link_to issues_group_path(@group), title: 'Issues' do
= icon('exclamation-circle fw')
%span
Issues
- if current_user
%span.count= Issue.opened.of_group(@group).count
= nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group), title: 'Merge Requests', data: {placement: 'right'} do
+ = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
= icon('tasks fw')
%span
Merge Requests
- if current_user
%span.count= MergeRequest.opened.of_group(@group).count
= nav_link(controller: [:group_members]) do
- = link_to group_group_members_path(@group), title: 'Members', data: {placement: 'right'} do
+ = link_to group_group_members_path(@group), title: 'Members' do
= icon('users fw')
%span
Members
- if can?(current_user, :admin_group, @group)
= nav_link(html_options: { class: "separate-item" }) do
- = link_to edit_group_path(@group), title: 'Settings', data: {placement: 'right'} do
+ = link_to edit_group_path(@group), title: 'Settings' do
= icon ('cogs fw')
%span
Settings
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index 8075fe32fbc..56a92fe9103 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -1,20 +1,20 @@
%ul.nav.nav-sidebar
= nav_link do
- = link_to group_path(@group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do
+ = link_to group_path(@group), title: 'Go to group', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to group
+ Go to group
%li.separate-item
%ul.sidebar-subnav
= nav_link(path: 'groups#edit') do
- = link_to edit_group_path(@group), title: 'Group Settings', data: {placement: 'right'} do
+ = link_to edit_group_path(@group), title: 'Group Settings' do
= icon ('pencil-square-o fw')
%span
Group Settings
= nav_link(path: 'groups#projects') do
- = link_to projects_group_path(@group), title: 'Projects', data: {placement: 'right'} do
+ = link_to projects_group_path(@group), title: 'Projects' do
= icon('folder fw')
%span
Projects
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 5a47b8e6db2..64b30783c05 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -1,59 +1,59 @@
%ul.nav.nav-sidebar
= nav_link do
- = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do
+ = link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to dashboard
+ Go to dashboard
%li.separate-item
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
- = link_to profile_path, title: 'Profile', data: {placement: 'right'} do
+ = link_to profile_path, title: 'Profile Settings' do
= icon('user fw')
%span
Profile Settings
= nav_link(controller: [:accounts, :two_factor_auths]) do
- = link_to profile_account_path, title: 'Account', data: {placement: 'right'} do
+ = link_to profile_account_path, title: 'Account' do
= icon('gear fw')
%span
Account
= nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new', 'applications#create']) do
- = link_to applications_profile_path, title: 'Applications', data: {placement: 'right'} do
+ = link_to applications_profile_path, title: 'Applications' do
= icon('cloud fw')
%span
Applications
= nav_link(controller: :emails) do
- = link_to profile_emails_path, title: 'Emails', data: {placement: 'right'} do
+ = link_to profile_emails_path, title: 'Emails' do
= icon('envelope-o fw')
%span
Emails
%span.count= current_user.emails.count + 1
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
- = link_to edit_profile_password_path, title: 'Password', data: {placement: 'right'} do
+ = link_to edit_profile_password_path, title: 'Password' do
= icon('lock fw')
%span
Password
= nav_link(controller: :notifications) do
- = link_to profile_notifications_path, title: 'Notifications', data: {placement: 'right'} do
+ = link_to profile_notifications_path, title: 'Notifications' do
= icon('inbox fw')
%span
Notifications
= nav_link(controller: :keys) do
- = link_to profile_keys_path, title: 'SSH Keys', data: {placement: 'right'} do
+ = link_to profile_keys_path, title: 'SSH Keys' do
= icon('key fw')
%span
SSH Keys
%span.count= current_user.keys.count
= nav_link(controller: :preferences) do
- = link_to profile_preferences_path, title: 'Preferences', data: {placement: 'right'} do
+ = link_to profile_preferences_path, title: 'Preferences' do
-# TODO (rspeicher): Better icon?
= icon('image fw')
%span
Preferences
= nav_link(path: 'profiles#audit_log') do
- = link_to audit_log_profile_path, title: 'Audit Log', data: {placement: 'right'} do
+ = link_to audit_log_profile_path, title: 'Audit Log' do
= icon('history fw')
%span
Audit Log
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index a218ec7486c..87a7707b095 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,67 +1,75 @@
%ul.nav.nav-sidebar
- if @project.group
= nav_link do
- = link_to group_path(@project.group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do
+ = link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to group
+ Go to group
- else
= nav_link do
- = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do
+ = link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to dashboard
+ Go to dashboard
%li.separate-item
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
- = link_to project_path(@project), title: 'Project', class: 'shortcuts-project', data: {placement: 'right'} do
+ = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
= icon('home fw')
%span
Project
= nav_link(path: 'projects#activity') do
- = link_to activity_project_path(@project), title: 'Project Activity', class: 'shortcuts-project-activity', data: {placement: 'right'} do
+ = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
= icon('dashboard fw')
%span
Activity
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree)) do
- = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree', data: {placement: 'right'} do
+ = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do
= icon('files-o fw')
%span
Files
- if project_nav_tab? :commits
- = nav_link(controller: %w(commit commits compare repositories tags branches)) do
- = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do
+ = nav_link(controller: %w(commit commits compare repositories tags branches releases)) do
+ = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
= icon('history fw')
%span
Commits
+ - if project_nav_tab? :builds
+ = nav_link(controller: %w(builds)) do
+ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
+ = icon('cubes fw')
+ %span
+ Builds
+ %span.count.builds_counter= @project.ci_builds.running_or_pending.count(:all)
+
- if project_nav_tab? :network
= nav_link(controller: %w(network)) do
- = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do
+ = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do
= icon('code-fork fw')
%span
Network
- if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do
- = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs', data: {placement: 'right'} do
+ = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do
= icon('area-chart fw')
%span
Graphs
- if project_nav_tab? :milestones
= nav_link(controller: :milestones) do
- = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones', data: {placement: 'right'} do
+ = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
= icon('clock-o fw')
%span
Milestones
- if project_nav_tab? :issues
= nav_link(controller: :issues) do
- = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do
+ = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do
= icon('exclamation-circle fw')
%span
Issues
@@ -70,50 +78,43 @@
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
- = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do
+ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
= icon('tasks fw')
%span
Merge Requests
%span.count.merge_counter= @project.merge_requests.opened.count
- - if @project.gitlab_ci?
- = nav_link(controller: [:ci, :project]) do
- = link_to ci_project_path(@project.gitlab_ci_project), title: 'Continuous Integration', data: {placement: 'right'} do
- = icon('building fw')
- %span
- Continuous Integration
-
- if project_nav_tab? :settings
= nav_link(controller: [:project_members, :teams]) do
- = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} do
+ = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
= icon('users fw')
%span
Members
- if project_nav_tab? :labels
= nav_link(controller: :labels) do
- = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels', data: {placement: 'right'} do
+ = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
= icon('tags fw')
%span
Labels
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
- = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki', data: {placement: 'right'} do
+ = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
= icon('book fw')
%span
Wiki
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
- = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets', data: {placement: 'right'} do
+ = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do
= icon('clipboard fw')
%span
Snippets
- if project_nav_tab? :settings
= nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do
- = link_to edit_project_path(@project), title: 'Settings', data: {placement: 'right'} do
+ = link_to edit_project_path(@project), title: 'Settings' do
= icon('cogs fw')
%span
Settings
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 857fb199957..f0b3f27b626 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -1,36 +1,67 @@
%ul.nav.nav-sidebar
= nav_link do
- = link_to project_path(@project), title: 'Back to project', data: {placement: 'right'}, class: 'back-link' do
+ = link_to project_path(@project), title: 'Go to project', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to project
+ Go to project
%li.separate-item
%ul.sidebar-subnav
= nav_link(path: 'projects#edit') do
- = link_to edit_project_path(@project), title: 'Project Settings', data: {placement: 'right'} do
+ = link_to edit_project_path(@project), title: 'Project Settings' do
= icon('pencil-square-o fw')
%span
Project Settings
= nav_link(controller: :deploy_keys) do
- = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys', data: {placement: 'right'} do
+ = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
= icon('key fw')
%span
Deploy Keys
= nav_link(controller: :hooks) do
- = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks', data: {placement: 'right'} do
+ = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do
= icon('link fw')
%span
Web Hooks
= nav_link(controller: :services) do
- = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services', data: {placement: 'right'} do
+ = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
= icon('cogs fw')
%span
Services
= nav_link(controller: :protected_branches) do
- = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches', data: {placement: 'right'} do
+ = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
= icon('lock fw')
%span
Protected Branches
+ - if @project.builds_enabled?
+ = nav_link(controller: :runners) do
+ = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
+ = icon('cog fw')
+ %span
+ Runners
+ = nav_link(controller: :variables) do
+ = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do
+ = icon('code fw')
+ %span
+ Variables
+ = nav_link path: 'triggers#index' do
+ = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
+ = icon('retweet fw')
+ %span
+ Triggers
+ = nav_link path: 'ci_web_hooks#index' do
+ = link_to namespace_project_ci_web_hooks_path(@project.namespace, @project), title: 'CI Web Hooks' do
+ = icon('link fw')
+ %span
+ CI Web Hooks
+ = nav_link path: 'ci_settings#edit' do
+ = link_to edit_namespace_project_ci_settings_path(@project.namespace, @project), title: 'CI Settings' do
+ = icon('building fw')
+ %span
+ CI Settings
+ = nav_link controller: 'ci_services' do
+ = link_to namespace_project_ci_services_path(@project.namespace, @project), title: 'CI Services' do
+ = icon('share fw')
+ %span
+ CI Services
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index ec209c38eed..3ca4c340406 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -40,7 +40,10 @@
Reply to this email directly or
#{link_to "view it on GitLab", @target_url}.
- else
- #{link_to "View it on GitLab", @target_url}
- = email_action @target_url
- - if @project && !@disable_footer
- You're receiving this notification because you are a member of the #{link_to_unless @target_url, @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} project team.
+ #{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.
+ You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
+ If you'd like to receive fewer emails, you can adjust your notification settings.
+
+ = email_action @target_url \ No newline at end of file
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 78dafcd8bfa..abf73bcc709 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -3,10 +3,11 @@
- sidebar "project" unless sidebar
- content_for :scripts_body_top do
+ - project = @target_project || @project
- if current_user
:javascript
- window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}";
- window.markdown_preview_path = "#{markdown_preview_namespace_project_path(@project.namespace, @project)}";
+ window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
+ window.markdown_preview_path = "#{markdown_preview_namespace_project_path(project.namespace, project)}";
- content_for :scripts_body do
= render "layouts/init_auto_complete" if current_user
diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml
index 3fd4b04ac84..00cb4aa24cc 100644
--- a/app/views/notify/_note_message.html.haml
+++ b/app/views/notify/_note_message.html.haml
@@ -1,2 +1,2 @@
%div
- = markdown(@note.note, reference_only_path: false)
+ = markdown(@note.note, pipeline: :email)
diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml
index 9db75bdb19e..34dbc60e19b 100644
--- a/app/views/notify/merged_merge_request_email.text.haml
+++ b/app/views/notify/merged_merge_request_email.text.haml
@@ -1,6 +1,6 @@
= "Merge Request ##{@merge_request.iid} was merged"
-Merge Request Url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
+Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to')
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index 53a068be52e..d3b799fca23 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -1,5 +1,5 @@
-if @issue.description
- = markdown(@issue.description, reference_only_path: false)
+ = markdown(@issue.description, pipeline: :email)
- if @issue.assignee_id.present?
%p
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 5b7dd117c16..90ebdfc3fe2 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -6,4 +6,4 @@
Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
-if @merge_request.description
- = markdown(@merge_request.description, reference_only_path: false)
+ = markdown(@merge_request.description, pipeline: :email)
diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml
index 3cd759f1f57..87b3ff7f0b3 100644
--- a/app/views/notify/project_was_moved_email.html.haml
+++ b/app/views/notify/project_was_moved_email.html.haml
@@ -1,5 +1,5 @@
%p
- Project was moved to another location
+ Project #{@old_path_with_namespace} was moved to another location
%p
The project is now located under
= link_to namespace_project_url(@project.namespace, @project) do
diff --git a/app/views/notify/project_was_moved_email.text.erb b/app/views/notify/project_was_moved_email.text.erb
index b3f18b35a4d..b2c5f71e465 100644
--- a/app/views/notify/project_was_moved_email.text.erb
+++ b/app/views/notify/project_was_moved_email.text.erb
@@ -1,4 +1,4 @@
-Project was moved to another location
+Project <%= @old_path_with_namespace %> was moved to another location
The project is now located under
<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml
index c19ac429d52..58af79716a7 100644
--- a/app/views/profiles/_event_table.html.haml
+++ b/app/views/profiles/_event_table.html.haml
@@ -1,16 +1,17 @@
-%table.table#audits
- %thead
- %tr
- %th Action
- %th When
-
- %tbody
- - events.each do |event|
+.table-holder
+ %table.table#audits
+ %thead
%tr
- %td
- %span
- Signed in with
- %b= event.details[:with]
- authentication
- %td #{time_ago_in_words event.created_at} ago
+ %th Action
+ %th When
+
+ %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 cd7b1b0fe03..319bdd57c39 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -23,10 +23,13 @@
%p.cgray
- if current_user.private_token
= text_field_tag "token", current_user.private_token, class: "form-control"
- %div
- = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default btn-build-token"
- else
%span You don`t have one yet. Click generate to fix it.
+
+ .form-actions
+ - if current_user.private_token
+ = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default btn-build-token"
+ - else
= f.submit 'Generate', class: "btn btn-default btn-build-token"
- unless current_user.ldap_user?
@@ -54,7 +57,8 @@
%p
Each time you log in you’ll be required to provide your username and
password as usual, plus a randomly-generated code from your phone.
- %div
+
+ .form-actions
= link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success'
- if button_based_providers.any?
@@ -81,15 +85,16 @@
%p
Changing your username will change path to all personal projects!
%div
- = f.text_field :username, required: true, class: 'form-control'
+ .input-group
+ .input-group-addon
+ = "#{root_url}u/"
+ = f.text_field :username, required: true, class: 'form-control'
&nbsp;
.loading-gif.hide
%p
= icon('spinner spin')
Saving new username
- %p.light
- = user_url(@user)
- %div
+ .form-actions
= f.submit 'Save username', class: "btn btn-warning"
- if signup_enabled?
@@ -104,7 +109,8 @@
- rp = current_user.personal_projects.count
- unless rp.zero?
%li #{pluralize rp, 'personal project'} will be removed and cannot be restored
- = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
+ .form-actions
+ = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
- else
- if @user.solo_owned_groups.present?
%p
diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml
index 2342936a5d5..0436c2213da 100644
--- a/app/views/profiles/applications.html.haml
+++ b/app/views/profiles/applications.html.haml
@@ -15,24 +15,25 @@
.pull-right
= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
- if @applications.any?
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Callback URL
- %th Clients
- %th
- %th
- %tbody
- - @applications.each do |application|
- %tr{:id => "application_#{application.id}"}
- %td= link_to application.name, oauth_application_path(application)
- %td
- - application.redirect_uri.split.each do |uri|
- %div= uri
- %td= application.access_tokens.count
- %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm'
- %td= render 'doorkeeper/applications/delete_form', application: application
+ .table-holder
+ %table.table.table-striped
+ %thead
+ %tr
+ %th Name
+ %th Callback URL
+ %th Clients
+ %th
+ %th
+ %tbody
+ - @applications.each do |application|
+ %tr{:id => "application_#{application.id}"}
+ %td= link_to application.name, oauth_application_path(application)
+ %td
+ - application.redirect_uri.split.each do |uri|
+ %div= uri
+ %td= application.access_tokens.count
+ %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm'
+ %td= render 'doorkeeper/applications/delete_form', application: application
.oauth-authorized-applications.prepend-top-20
- if user_oauth_applications?
@@ -40,29 +41,30 @@
Authorized applications
- if @authorized_tokens.any?
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Authorized At
- %th Scope
- %th
- %tbody
- - @authorized_apps.each do |app|
- - token = app.authorized_tokens.order('created_at desc').first
- %tr{:id => "application_#{app.id}"}
- %td= app.name
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', application: app
- - @authorized_anonymous_tokens.each do |token|
+ .table-holder
+ %table.table.table-striped
+ %thead
%tr
- %td
- Anonymous
- %div.help-block
- %em Authorization was granted by entering your username and password in the application.
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', token: token
+ %th Name
+ %th Authorized At
+ %th Scope
+ %th
+ %tbody
+ - @authorized_apps.each do |app|
+ - token = app.authorized_tokens.order('created_at desc').first
+ %tr{:id => "application_#{app.id}"}
+ %td= app.name
+ %td= token.created_at
+ %td= token.scopes
+ %td= render 'doorkeeper/authorized_applications/delete_form', application: app
+ - @authorized_anonymous_tokens.each do |token|
+ %tr
+ %td
+ Anonymous
+ %div.help-block
+ %em Authorization was granted by entering your username and password in the application.
+ %td= token.created_at
+ %td= token.scopes
+ %td= render 'doorkeeper/authorized_applications/delete_form', token: token
- else
%p.light You don't have any authorized applications
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index b76a5b636ac..2a8800de60e 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -1,5 +1,5 @@
%div
- = form_for [:profile, @key], html: { class: 'form-horizontal' } do |f|
+ = form_for [:profile, @key], html: { class: 'form-horizontal js-requires-input' } do |f|
- if @key.errors.any?
.alert.alert-danger
%ul
@@ -9,12 +9,11 @@
.form-group
= f.label :key, class: 'control-label'
.col-sm-10
- = f.text_area :key, class: "form-control", rows: 8
+ = f.text_area :key, class: "form-control", rows: 8, autofocus: true, required: true
.form-group
= f.label :title, class: 'control-label'
- .col-sm-10= f.text_field :title, class: "form-control"
+ .col-sm-10= f.text_field :title, class: "form-control", required: true
.form-actions
= f.submit 'Add key', class: "btn btn-create"
= link_to "Cancel", profile_keys_path, class: "btn btn-cancel"
-
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index e0ae4d9720f..0ca8bd95157 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -18,5 +18,6 @@
%code.key-fingerprint= @key.fingerprint
%pre.well-pre
= @key.key
- .pull-right
- = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key"
+ .col-md-12
+ .pull-right
+ = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key"
diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml
index ef0075aad3b..8c9d546af4c 100644
--- a/app/views/profiles/keys/_key_table.html.haml
+++ b/app/views/profiles/keys/_key_table.html.haml
@@ -1,6 +1,6 @@
- is_admin = defined?(admin) ? true : false
-.panel.panel-default
- - if @keys.any?
+- if @keys.any?
+ .table-holder
%table.table
%thead.panel-heading
%tr
@@ -11,9 +11,9 @@
%tbody
- @keys.each do |key|
= render 'profiles/keys/key', key: key, is_admin: is_admin
- - else
- .nothing-here-block
- - if is_admin
- User has no ssh keys
- - else
- There are no SSH keys with access to your account.
+- else
+ .nothing-here-block
+ - if is_admin
+ User has no ssh keys
+ - else
+ There are no SSH keys with access to your account.
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 14adba1c797..17a4195030e 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -3,7 +3,9 @@
.gray-content-block.top-block
.pull-right
- = link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new"
+ = link_to new_profile_key_path, class: "btn btn-new" do
+ = icon('plus')
+ Add SSH Key
.oneline
Before you can add an SSH key you need to
= link_to "generate it.", help_page_path("ssh", "README")
diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml
index 2c85d2a9b2b..742c5c4b68d 100644
--- a/app/views/profiles/notifications/_settings.html.haml
+++ b/app/views/profiles/notifications/_settings.html.haml
@@ -14,4 +14,4 @@
= form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do
= hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type')
= hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id')
- = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'trigger-submit'
+ = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'form-control trigger-submit'
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 8eebd96b674..0bcadc965fa 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -50,12 +50,10 @@
Watch
%p You will receive notifications for any activity
- .form-actions
+ .gray-content-block
= f.submit 'Save changes', class: "btn btn-create"
-.clearfix
- %hr
-.row.all-notifications
+.row.all-notifications.prepend-top-default
.col-md-6
%p
You can also specify notification level per group or per project.
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 60289bfe7cd..877589dc390 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -33,6 +33,13 @@
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')
@@ -45,6 +52,6 @@
.col-sm-10
= f.select :project_view, project_view_choices, {}, class: 'form-control'
.help-block
- Choose what content you want to see when visit project page
+ Choose what content you want to see on a project's home page.
.panel-footer
- = f.submit 'Save', class: 'btn btn-save'
+ = f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
index 6c4b0ce757d..4433cab7782 100644
--- a/app/views/profiles/preferences/update.js.erb
+++ b/app/views/profiles/preferences/update.js.erb
@@ -2,6 +2,13 @@
$('body').removeClass('<%= Gitlab::Themes.body_classes %>')
$('body').addClass('<%= user_application_theme %>')
+// Toggle container-fluid class
+if ('<%= current_user.layout %>' === 'fluid') {
+ $('.content-wrapper').find('.container-fluid').removeClass('container-limited')
+} else {
+ $('.content-wrapper').find('.container-fluid').addClass('container-limited')
+}
+
// Re-enable the "Save" button
$('input[type=submit]').enable()
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 47412e2ef0c..9459d8a6295 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -43,7 +43,7 @@
.form-group
= f.label :public_email, class: "control-label"
.col-sm-10
- = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show in profile'}, class: "form-control"
+ = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2"
%span.help-block This email will be displayed on your public profile.
.form-group
= f.label :skype, class: "control-label"
@@ -68,7 +68,7 @@
.col-md-5
.light-well
- = image_tag avatar_icon(@user.email, 160), alt: '', class: 'avatar s160'
+ = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
.clearfix
.profile-avatar-form-option
@@ -96,8 +96,6 @@
= link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
- .row
- .col-md-7
- .form-group
- .col-sm-offset-2.col-sm-10
- = f.submit 'Save changes', class: "btn btn-success"
+ .form-actions
+ = f.submit 'Save changes', class: "btn btn-success"
+ = link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 1261f6254d7..101880bd105 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,14 +1,12 @@
-= render 'projects/last_push'
.gray-content-block.activity-filter-block
- if current_user
- %ul.nav.nav-pills.event_filter.pull-right
- %li
- = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'rss-btn' do
- %i.fa.fa-rss
+ .pull-right
+ = 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
= render 'shared/event_filter'
.content_list{:"data-href" => activity_project_path(@project)}
= spinner
-:coffeescript
- new Activities()
+:javascript
+ new Activities();
diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml
index 35f7e7bb34b..2fd3d9e1be4 100644
--- a/app/views/projects/_commit_button.html.haml
+++ b/app/views/projects/_commit_button.html.haml
@@ -1,6 +1,4 @@
.form-actions
- .commit-button-annotation
- = button_tag 'Commit Changes',
- class: 'btn commit-btn js-commit-button btn-create'
+ = button_tag 'Commit Changes', class: 'btn commit-btn js-commit-button btn-create'
= link_to 'Cancel', cancel_path,
class: 'btn btn-cancel', data: {confirm: leave_edit_message}
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
new file mode 100644
index 00000000000..fa978325ddd
--- /dev/null
+++ b/app/views/projects/_files.html.haml
@@ -0,0 +1,6 @@
+#tree-holder.tree-holder.clearfix
+ .gray-content-block.second-block
+ = render 'projects/tree/tree_header', tree: @tree
+
+ = render 'projects/tree/tree_content', tree: @tree
+
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 6e53f55b0ab..c1669ac046b 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,5 +1,5 @@
- empty_repo = @project.empty_repo?
-.project-home-panel.clearfix{:class => ("empty-project" if empty_repo)}
+.project-home-panel.cover-block.clearfix{:class => ("empty-project" if empty_repo)}
.project-identicon-holder
= project_icon(@project, alt: '', class: 'project-avatar avatar s90')
.project-home-desc
@@ -12,22 +12,29 @@
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')
.project-repo-buttons
- = render 'projects/buttons/star'
-
- - unless empty_repo
+ .split-one
+ = render 'projects/buttons/star'
= render 'projects/buttons/fork'
- - if can? current_user, :download_code, @project
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do
- = icon('download fw')
- Download
+ = render "shared/clone_panel"
- = render 'projects/buttons/notifications'
-
- = render 'projects/buttons/dropdown'
+ .split-repo-buttons
+ = render "projects/buttons/download"
+ = render 'projects/buttons/dropdown'
- = render "shared/clone_panel"
+ = render 'projects/buttons/notifications'
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
new file mode 100644
index 00000000000..7e1ee2b7fc1
--- /dev/null
+++ b/app/views/projects/_last_commit.html.haml
@@ -0,0 +1,12 @@
+.project-last-commit
+ - ci_commit = project.ci_commit(commit.sha)
+ - if ci_commit
+ = link_to ci_status_path(ci_commit), class: "ci-status ci-#{ci_commit.status}" do
+ = ci_status_icon(ci_commit)
+ = ci_commit.status
+
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
+ = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
+ &middot;
+ #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by
+ = commit_author_link(commit, avatar: true, size: 24)
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 507757f6a2b..54c818baaf4 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -2,23 +2,24 @@
.md-header.clearfix
%ul.center-top-menu
%li.active
- = link_to '#md-write-holder', class: 'js-md-write-button', tabindex: '-1' do
+ %a.js-md-write-button(href="#md-write-holder" tabindex="-1")
Write
%li
- = link_to '#md-preview-holder', class: 'js-md-preview-button', tabindex: '-1' do
+ %a.js-md-preview-button(href="#md-preview-holder" tabindex="-1")
Preview
- - if defined?(referenced_users) && referenced_users
- %span.referenced-users.pull-left.hide
+ %div
+ .md-write-holder
+ = yield
+ .md.md-preview-holder.hide
+ .js-md-preview{class: (preview_class if defined?(preview_class))}
+
+ - if defined?(referenced_users) && referenced_users
+ %div.referenced-users.hide
+ %span
= icon('exclamation-triangle')
You are about to add
%strong
%span.js-referenced-users-count 0
people
to the discussion. Proceed with caution.
-
- %div
- .md-write-holder
- = yield
- .md.md-preview-holder.hide
- .js-md-preview{class: (preview_class if defined?(preview_class))}
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index 5038edb95ed..d1191928d4f 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -1,24 +1,22 @@
- if readme = @repository.readme
- %article.readme-holder#README
- .clearfix
- .pull-right
- &nbsp;
- - if can?(current_user, :push_code, @project)
- = link_to namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light' do
- %i.fa.fa-pencil
- .wiki
+ %article.readme-holder
+ .pull-right
+ - if can?(current_user, :push_code, @project)
+ = link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme'
+ .file-content.wiki
= cache(readme_cache_key) do
= render_readme(readme)
- else
- %h3.page-title
- This project does not have README yet
- - if can?(current_user, :push_code, @project)
- %p.slead
- A
- %code README
- file contains information about other files in a repository and is commonly
- distributed with computer software, forming part of its documentation.
- %br
- We recommend you to
- = link_to "add README", new_readme_path, class: 'underlined-link'
- file to the repository and GitLab will render it here instead of this message.
+ .gray-content-block.second-block.center
+ %h3.page-title
+ This project does not have README yet
+ - if can?(current_user, :push_code, @project)
+ %p
+ A
+ %code README
+ file contains information about other files in a repository and is commonly
+ distributed with computer software, forming part of its documentation.
+ %p
+ We recommend you to
+ = link_to "add README", new_readme_path, class: 'underlined-link'
+ file to the repository and GitLab will render it here instead of this message.
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 6a41cdbc907..7e6301abde8 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,10 +1,13 @@
.zennable
- %input#zen-toggle-comment.zen-toggle-comment{ tabindex: '-1', type: 'checkbox' }
+ %input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox")
.zen-backdrop
- classes << ' js-gfm-input markdown-area'
- = f.text_area attr, class: classes, placeholder: ''
- = link_to nil, class: 'zen-enter-link', tabindex: '-1' do
- %i.fa.fa-expand
+ - if defined?(f) && f
+ = f.text_area attr, class: classes, placeholder: ''
+ - else
+ = text_area_tag attr, nil, class: classes, placeholder: ''
+ %a.zen-enter-link(tabindex="-1" href="#")
+ = icon('expand')
Edit in fullscreen
- = link_to nil, class: 'zen-leave-link' do
- %i.fa.fa-compress
+ %a.zen-leave-link(href="#")
+ = icon('compress')
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index 555ed76426d..69fa4ad37c4 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -1,4 +1,6 @@
- page_title "Activity"
- header_title project_title(@project, "Activity", activity_project_path(@project))
+= render 'projects/last_push'
+
= render 'projects/activity'
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 6518c4173e1..8d9ec068a43 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -6,7 +6,7 @@
#tree-holder.tree-holder
.file-holder
.file-title
- %i.fa.fa-file
+ = blob_icon @blob.mode, @blob.name
%strong
= @path
%small= number_to_human_size @blob.size
@@ -43,4 +43,3 @@
- blame_group[:lines].each do |line|
:erb
<%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %>
-
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index 373b3a0c5b0..ba3e0c3c590 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -19,4 +19,4 @@
- if allowed_tree_edit?
.btn-group{ role: "group" }
%button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace
- %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Remove
+ %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Delete
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index b4c7d8b9b71..42f632b38ef 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -1,25 +1,28 @@
-%ul.breadcrumb.repo-breadcrumb
- %li
- %i.fa.fa-angle-right
- = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
- = @project.path
- - tree_breadcrumbs(@tree, 6) do |title, path|
+.gray-content-block.top-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'blob', path: @path
+
+ %ul.breadcrumb.repo-breadcrumb
%li
- - if path
- - if path.end_with?(@path)
- = link_to namespace_project_blob_path(@project.namespace, @project, path) do
- %strong
- = truncate(title, length: 40)
+ = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
+ = @project.path
+ - tree_breadcrumbs(@tree, 6) do |title, path|
+ %li
+ - if path
+ - if path.end_with?(@path)
+ = link_to namespace_project_blob_path(@project.namespace, @project, path) do
+ %strong
+ = truncate(title, length: 40)
+ - else
+ = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- else
- = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- - else
- = link_to title, '#'
+ = link_to title, '#'
%ul.blob-commit-info.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
= render blob_commit, project: @project
-%div#tree-content-holder.tree-content-holder
+%div#blob-content-holder.blob-content-holder
%article.file-holder
.file-title
= blob_icon blob.mode, blob.name
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 9c3e1703c89..333f5d470ed 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -1,19 +1,19 @@
-.file-holder.file
- .file-title
+.file-holder.file.append-bottom-default
+ .file-title.clearfix
.editor-ref
- %i.fa.fa-code-fork
+ = icon('code-fork')
= ref
%span.editor-file-name
- - if @path
- %span.monospace
- = @path
+ = @path
- - if current_action?(:new) || current_action?(:create)
+ - if current_action?(:new) || current_action?(:create)
+ %span.editor-file-name
\/
- = text_field_tag 'file_name', params[:file_name], placeholder: "File name",
- required: true, class: 'form-control new-file-name'
- .pull-right
- = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control'
+ = text_field_tag 'file_name', params[:file_name], placeholder: "File name",
+ required: true, class: 'form-control new-file-name js-quick-submit'
+
+ .pull-right
+ = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
.file-content.code
%pre.js-edit-mode-pane#editor
diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml
new file mode 100644
index 00000000000..fc6c9f5fd09
--- /dev/null
+++ b/app/views/projects/blob/_new_dir.html.haml
@@ -0,0 +1,21 @@
+#modal-create-new-dir.modal
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %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-group
+ = label_tag :dir_name, 'Directory name', class: 'control-label'
+ .col-sm-10
+ = text_field_tag :dir_name, params[:dir_name], required: true, class: 'form-control'
+
+ = render 'shared/new_commit_form', placeholder: "Add new directory"
+
+ .form-actions
+ = submit_tag "Create directory", class: 'btn btn-create'
+ = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+
+:javascript
+ new NewCommitForm($('.js-create-dir-form'))
diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml
index cae5ff01099..1cf19a7d3db 100644
--- a/app/views/projects/blob/_remove.html.haml
+++ b/app/views/projects/blob/_remove.html.haml
@@ -3,16 +3,16 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3.page-title Remove #{@blob.name}
- %p.light
- From branch
- %strong= @ref
+ %h3.page-title Delete #{@blob.name}
.modal-body
- = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-requires-input' do
- = render 'shared/commit_message_container', params: params,
- placeholder: 'Removed this file because...'
+ = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-requires-input' do
+ = render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}"
+
.form-group
.col-sm-offset-2.col-sm-10
- = button_tag 'Remove file', class: 'btn btn-remove btn-remove-file'
+ = button_tag 'Delete file', class: 'btn btn-remove btn-remove-file'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+
+:javascript
+ new NewCommitForm($('.js-replace-blob-form'))
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 1a1df127703..ecc90a30e78 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -4,11 +4,8 @@
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title #{title}
- %p.light
- From branch
- %strong= @ref
.modal-body
- = form_tag form_path, method: method, class: 'blob-file-upload-form-js form-horizontal' do
+ = form_tag form_path, method: method, class: 'js-upload-blob-form form-horizontal' do
.dropzone
.dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light
@@ -16,13 +13,14 @@
= link_to 'click to upload', '#', class: "markdown-selector"
%br
.dropzone-alerts{class: "alert alert-danger data", style: "display:none"}
- = render 'shared/commit_message_container', params: params,
- placeholder: placeholder
- .form-group
- .col-sm-offset-2.col-sm-10
- = button_tag button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all'
- = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
-:coffeescript
- disableButtonIfEmptyField $('.blob-file-upload-form-js').find('#commit_message'), '.btn-upload-file'
- new BlobFileDropzone($('.blob-file-upload-form-js'), '#{method}')
+ = render 'shared/new_commit_form', placeholder: placeholder
+
+ .form-actions
+ = button_tag button_title, class: 'btn btn-small btn-create btn-upload-file', id: 'submit-all'
+ = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+
+:javascript
+ disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file');
+ new BlobFileDropzone($('.js-upload-blob-form'), '#{method}');
+ new NewCommitForm($('.js-upload-blob-form'))
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index a811adc5094..a47fe7ede80 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -5,23 +5,17 @@
%ul.center-top-menu.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
- %i.fa.fa-edit
- Edit file
+ = icon('edit')
+ Edit File
%li
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
- %i.fa.fa-eye
+ = icon('eye')
= editing_preview_title(@blob.name)
- = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input') do
+ = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input js-edit-blob-form') do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
- = render 'shared/commit_message_container', params: params, placeholder: "Update #{@blob.name}"
-
- .form-group.branch
- = label_tag 'branch', class: 'control-label' do
- Branch
- .col-sm-10
- = text_field_tag 'new_branch', @ref, class: "form-control"
+ = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit', @last_commit
= hidden_field_tag 'content', '', id: "file-content"
@@ -30,3 +24,4 @@
:javascript
blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}")
+ new NewCommitForm($('.js-edit-blob-form'))
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 1950586b112..167fa615182 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,26 +1,13 @@
- page_title "New File", @path.presence, @ref
= render "header_title"
-.gray-content-block.top-block
- Create a new file or
- = link_to 'upload', '#modal-upload-blob',
- { class: 'upload-link', 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'}
- an existing one
-
-= render 'projects/blob/upload', title: 'Upload', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post
+%h3.page-title
+ New File
.file-editor
- = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file js-requires-input') do
+ = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-requires-input') do
= render 'projects/blob/editor', ref: @ref
- = render 'shared/commit_message_container', params: params,
- placeholder: 'Add new file'
-
- - unless @project.empty_repo?
- .form-group.branch
- = label_tag 'branch', class: 'control-label' do
- Branch
- .col-sm-10
- = text_field_tag 'new_branch', @ref, class: "form-control"
+ = render 'shared/new_commit_form', placeholder: "Add new file"
= hidden_field_tag 'content', '', id: 'file-content'
= render 'projects/commit_button', ref: @ref,
@@ -28,3 +15,4 @@
:javascript
blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null)
+ new NewCommitForm($('.js-new-blob-form'))
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index fa4be4a1bc4..b7276868ce6 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -3,9 +3,6 @@
= render 'projects/last_push'
-%div.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'blob', path: @path
-
%div#tree-holder.tree-holder
= render 'blob', blob: @blob
@@ -13,6 +10,4 @@
= render 'projects/blob/remove'
- title = "Replace #{@blob.name}"
- = render 'projects/blob/upload', title: title, placeholder: title,
- button_title: 'Replace file', form_path: namespace_project_update_blob_path(@project.namespace, @project, @id),
- method: :put
+ = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index cc0ec9483d2..3f95e2a1bf6 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -26,7 +26,7 @@
Compare
- if can_remove_branch?(@project, branch.name)
- = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do
+ = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row', method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" }, remote: true do
= icon("trash-o")
- if commit
diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml
index 68326e65d85..9fe65cbb104 100644
--- a/app/views/projects/branches/_commit.html.haml
+++ b/app/views/projects/branches/_commit.html.haml
@@ -1,5 +1,5 @@
-.branch-commit.light
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
+.branch-commit
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-id monospace"
&middot;
%span.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 03ade02a0c8..204def60794 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -5,7 +5,7 @@
.pull-right
- if can? current_user, :push_code, @project
= link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do
- %i.fa.fa-add-sign
+ = icon('plus')
New branch
&nbsp;
.dropdown.inline
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index f5577042ca4..31943a2407a 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -6,25 +6,27 @@
%button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
= @error
%h3.page-title
- %i.fa.fa-code-fork
- New branch
+ New Branch
+%hr
+
= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-requires-input" do
.form-group
- = label_tag :branch_name, 'Name for new branch', class: 'control-label'
+ = label_tag :branch_name, nil, class: 'control-label'
.col-sm-10
- = text_field_tag :branch_name, params[:branch_name], placeholder: 'enter new branch name', required: true, tabindex: 1, class: 'form-control'
+ = text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control'
.form-group
= label_tag :ref, 'Create from', class: 'control-label'
.col-sm-10
- = text_field_tag :ref, params[:ref], placeholder: 'existing branch name, tag or commit SHA', required: true, tabindex: 2, class: 'form-control'
+ = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control'
+ .help-block Existing branch name, tag, or commit SHA
.form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', namespace_project_branches_path(@project.namespace, @project), class: 'btn btn-cancel'
:javascript
- var availableTags = #{@project.repository.ref_names.to_json};
+ var availableRefs = #{@project.repository.ref_names.to_json};
$("#ref").autocomplete({
- source: availableTags,
+ source: availableRefs,
minLength: 1
});
diff --git a/app/views/projects/builds/_header_title.html.haml b/app/views/projects/builds/_header_title.html.haml
new file mode 100644
index 00000000000..082dab1f5b0
--- /dev/null
+++ b/app/views/projects/builds/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Builds", project_builds_path(@project))
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
new file mode 100644
index 00000000000..dab7164153f
--- /dev/null
+++ b/app/views/projects/builds/index.html.haml
@@ -0,0 +1,53 @@
+- page_title "Builds"
+= render "header_title"
+
+.project-issuable-filter
+ .controls
+ - if @ci_project && current_user && can?(current_user, :manage_builds, @project)
+ .pull-left.hidden-xs
+ - if @all_builds.running_or_pending.any?
+ = link_to 'Cancel all', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
+
+ %ul.center-top-menu
+ %li{class: ('active' if @scope.nil?)}
+ = link_to project_builds_path(@project) do
+ Running
+ %span.badge.js-running-count= @all_builds.running_or_pending.count(:id)
+
+ %li{class: ('active' if @scope == 'finished')}
+ = link_to project_builds_path(@project, scope: :finished) do
+ Finished
+ %span.badge.js-running-count= @all_builds.finished.count(:id)
+
+ %li{class: ('active' if @scope == 'all')}
+ = link_to project_builds_path(@project, scope: :all) do
+ All
+ %span.badge.js-totalbuilds-count= @all_builds.count(:id)
+
+.gray-content-block
+ #{(@scope || 'running').capitalize} builds from this project
+
+%ul.content-list
+ - if @builds.blank?
+ %li
+ .nothing-here-block No builds to show
+ - else
+ .table-holder
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Commit
+ %th Ref
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ %th
+
+ - @builds.each do |build|
+ = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, allow_retry: true
+
+ = paginate @builds, theme: 'gitlab'
+
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
new file mode 100644
index 00000000000..907e1ce10bd
--- /dev/null
+++ b/app/views/projects/builds/show.html.haml
@@ -0,0 +1,184 @@
+- page_title "#{@build.name} (#{@build.id})", "Builds"
+= render "header_title"
+
+.build-page
+ .gray-content-block
+ Build ##{@build.id} for commit
+ %strong.monospace
+ = link_to @build.commit.short_sha, ci_status_path(@build.commit)
+ from
+ = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref)
+
+ #up-build-trace
+ - if @commit.matrix_for_ref?(@build.ref)
+ %ul.center-top-menu.build-top-menu
+ - @commit.latest_builds_for_ref(@build.ref).each do |build|
+ %li{class: ('active' if build == @build) }
+ = link_to namespace_project_build_path(@project.namespace, @project, build) do
+ = ci_icon_for_status(build.status)
+ %span
+ - if build.name
+ = build.name
+ - else
+ = build.id
+
+
+ - if @build.retried?
+ %li.active
+ %a
+ Build ##{@build.id}
+ &middot;
+ %i.fa.fa-warning
+ This build was retried.
+
+ .gray-content-block.second-block
+ .build-head
+ .clearfix
+ = ci_status_with_icon(@build.status)
+ - if @build.duration
+ %span
+ %i.fa.fa-time
+ #{duration_in_words(@build.finished_at, @build.started_at)}
+ .pull-right
+ #{time_ago_with_tooltip(@build.finished_at) if @build.finished_at}
+
+ - if @build.show_warning?
+ - unless @build.any_runners_online?
+ .bs-callout.bs-callout-warning
+ %p
+ - if no_runners_for_project?(@build.project)
+ This build is stuck, because the project doesn't have any runners online assigned to it.
+ - elsif @build.tags.any?
+ This build is stuck, because you don't have any active runners online with any of these tags assigned to them:
+ - @build.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - else
+ This build is stuck, because you don't have any active runners that can run this build.
+
+ %br
+ Go to
+ = link_to namespace_project_runners_path(@build.gl_project.namespace, @build.gl_project) do
+ Runners page
+
+ .row.prepend-top-default
+ .col-md-9
+ .clearfix
+ - if @build.active?
+ .autoscroll-container
+ %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
+ .clearfix
+ .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
+ %div#down-build-trace
+
+ .col-md-3
+ - if @build.coverage
+ .build-widget
+ %h4.title
+ Test coverage
+ %h1 #{@build.coverage}%
+
+ - if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url
+ .build-widget.center
+ = link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary'
+
+ .build-widget
+ %h4.title
+ Build ##{@build.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
+ %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
+ %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}", ci_admin_runner_path(@build.runner.id)
+ - elsif @build.runner
+ \##{@build.runner.id}
+
+ - if @build.trigger_request
+ .build-widget
+ %h4.title
+ Trigger
+
+ %p
+ %span.attr-name Token:
+ #{@build.trigger_request.trigger.short_token}
+
+ - if @build.trigger_request.variables
+ %p
+ %span.attr-name Variables:
+
+ %code
+ - @build.trigger_request.variables.each do |key, value|
+ #{key}=#{value}
+
+ .build-widget
+ %h4.title
+ Commit
+ .pull-right
+ %small
+ = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace"
+ %p
+ %span.attr-name Branch:
+ = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref)
+ %p
+ %span.attr-name Author:
+ #{@build.commit.git_author_name}
+ %p
+ %span.attr-name Message:
+ #{@build.commit.git_commit_message}
+
+ - if @build.tags.any?
+ .build-widget
+ %h4.title
+ Tags
+ - @build.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+
+ - if @builds.present?
+ .build-widget
+ %h4.title #{pluralize(@builds.count(:id), "other build")} for
+ = succeed ":" do
+ = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace"
+ %table.table.builds
+ - @builds.each_with_index do |build, i|
+ %tr.build
+ %td
+ = ci_icon_for_status(build.status)
+ %td
+ = link_to namespace_project_build_path(@project.namespace, @project, build) do
+ - if build.name
+ = build.name
+ - else
+ %span ##{build.id}
+
+ %td.status= build.status
+
+
+ :javascript
+ new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}")
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
new file mode 100644
index 00000000000..14ee2263b7d
--- /dev/null
+++ b/app/views/projects/buttons/_download.html.haml
@@ -0,0 +1,4 @@
+- unless @project.empty_repo?
+ - if can? current_user, :download_code, @project
+ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', rel: 'nofollow', title: "Download ZIP" do
+ = icon('download')
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index bc7625e8989..b277b765b6b 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,11 +1,11 @@
- if current_user
%span.dropdown
- %a.dropdown-toggle.btn.btn-new{href: '#', "data-toggle" => "dropdown"}
+ %a.dropdown-new.btn.btn-new{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- if can?(current_user, :create_issue, @project)
%li
- = link_to url_for_new_issue do
+ = link_to url_for_new_issue(@project, only_path: true) do
= icon('exclamation-circle fw')
New issue
- if can?(current_user, :create_merge_request, @project)
@@ -21,6 +21,10 @@
- if can?(current_user, :push_code, @project)
%li.divider
%li
+ = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), title: 'New file' do
+ = icon('file fw')
+ New file
+ %li
= link_to new_namespace_project_branch_path(@project.namespace, @project) do
= icon('code-fork fw')
New branch
@@ -28,5 +32,3 @@
= link_to new_namespace_project_tag_path(@project.namespace, @project) do
= icon('tags fw')
New tag
-
-
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 854c154824d..2d3abf09051 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -1,13 +1,13 @@
-- if current_user && can?(current_user, :fork_project, @project)
- - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
- = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn' do
- = icon('code-fork fw')
- Fork
- %span.count
- = @project.forks_count
- - else
- = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn' do
- = icon('code-fork fw')
- Fork
- %span.count
- = @project.forks_count
+- unless @project.empty_repo?
+ - if current_user && can?(current_user, :fork_project, @project)
+ - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
+ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has_tooltip' do
+ = icon('code-fork fw')
+ Fork
+ %span.count
+ = @project.forks_count
+ - else
+ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do
+ = icon('code-fork fw')
+ %span.count
+ = @project.forks_count
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
index 57f764178d5..3e83ec3912f 100644
--- a/app/views/projects/buttons/_notifications.html.haml
+++ b/app/views/projects/buttons/_notifications.html.haml
@@ -1,14 +1,20 @@
-- return unless @membership
+- case @membership
+- when ProjectMember
+ = form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do
+ = hidden_field_tag :notification_type, 'project'
+ = hidden_field_tag :notification_id, @membership.id
+ = hidden_field_tag :notification_level
+ %span.dropdown
+ %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"}
+ = icon('bell')
+ = notification_label(@membership)
+ = icon('angle-down')
+ %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
+ - Notification.project_notification_levels.each do |level|
+ = notification_list_item(level, @membership)
-= form_tag profile_notifications_path, method: :put, remote: true, class: 'inline-form', id: 'notification-form' do
- = hidden_field_tag :notification_type, 'project'
- = hidden_field_tag :notification_id, @membership.id
- = hidden_field_tag :notification_level
- %span.dropdown
- %a.dropdown-toggle.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"}
- = icon('bell')
- = notification_label(@membership)
- = icon('angle-down')
- %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- - Notification.project_notification_levels.each do |level|
- = notification_list_item(level, @membership)
+- when GroupMember
+ .btn.disabled.notifications-btn.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
+ = icon('bell')
+ = notification_label(@membership)
+ = icon('angle-down')
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 5d7df5ae099..41a3ec6d90f 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,22 +1,19 @@
- if current_user
- = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star', method: :post, remote: true do
+ = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has_tooltip', method: :post, remote: true, title: "Star project" do
= icon('star fw')
- - if current_user.starred?(@project)
- Unstar
- - else
- Star
%span.count
= @project.star_count
- :coffeescript
- $('.project-home-panel .toggle-star').on 'ajax:success', (e, data, status, xhr) ->
- $(@).replaceWith(data.html)
- .on 'ajax:error', (e, xhr, status, error) ->
- new Flash('Star toggle failed. Try again later.', 'alert')
+ :javascript
+ $('.project-home-panel .toggle-star').on('ajax:success', function (e, data, status, xhr) {
+ $(this).replaceWith(data.html);
+ })
+ .on('ajax:error', function (e, xhr, status, error) {
+ new Flash('Star toggle failed. Try again later.', 'alert');
+ });
- else
= link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do
= icon('star fw')
- Star
%span.count
= @project.star_count
diff --git a/app/views/ci/services/_form.html.haml b/app/views/projects/ci_services/_form.html.haml
index 9110aaa0528..397832e56db 100644
--- a/app/views/ci/services/_form.html.haml
+++ b/app/views/projects/ci_services/_form.html.haml
@@ -4,13 +4,10 @@
%p= @service.description
-.back-link
- = link_to ci_project_services_path(@project) do
- &larr; to services
%hr
-= form_for(@service, as: :service, url: ci_project_service_path(@project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f|
+= form_for(@service, as: :service, url: namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f|
- if @service.errors.any?
.alert.alert-danger
%ul
@@ -54,4 +51,4 @@
= f.submit 'Save', class: 'btn btn-save'
&nbsp;
- if @service.valid? && @service.activated? && @service.can_test?
- = link_to 'Test settings', test_ci_project_service_path(@project, @service.to_param), class: 'btn'
+ = link_to 'Test settings', test_namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), class: 'btn'
diff --git a/app/views/projects/ci_services/edit.html.haml b/app/views/projects/ci_services/edit.html.haml
new file mode 100644
index 00000000000..dacb6b4f6f4
--- /dev/null
+++ b/app/views/projects/ci_services/edit.html.haml
@@ -0,0 +1,2 @@
+- page_title @service.title, "CI Services"
+= render 'form'
diff --git a/app/views/ci/services/index.html.haml b/app/views/projects/ci_services/index.html.haml
index 37e5723b541..3f26c7851d8 100644
--- a/app/views/ci/services/index.html.haml
+++ b/app/views/projects/ci_services/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "CI Services"
%h3.page-title Project services
%p.light Project services allow you to integrate GitLab CI with other applications
@@ -6,14 +7,14 @@
%tr
%th
%th Service
- %th Desription
+ %th Description
%th Last edit
- @services.sort_by(&:title).each do |service|
%tr
%td
= boolean_to_icon service.activated?
%td
- = link_to edit_ci_project_service_path(@project, service.to_param) do
+ = link_to edit_namespace_project_ci_service_path(@project.namespace, @project, service.to_param) do
%strong= service.title
%td
= service.description
diff --git a/app/views/ci/projects/_form.html.haml b/app/views/projects/ci_settings/_form.html.haml
index e782fd8a0f7..ee6b8885e2d 100644
--- a/app/views/ci/projects/_form.html.haml
+++ b/app/views/projects/ci_settings/_form.html.haml
@@ -1,17 +1,36 @@
+%h3.page-title
+ CI settings
+%hr
.bs-callout.help-callout
%p
If you want to test your .gitlab-ci.yml, you can use special tool - #{link_to "Lint", ci_lint_path}
%p
- Edit your
- #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@project)}
+ Edit your
+ #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@ci_project)}
-= nested_form_for [:ci, @project], html: { class: 'form-horizontal' } do |f|
- - if @project.errors.any?
+- unless @project.empty_repo?
+ %p
+ Paste build status image for #{@repository.root_ref} with next link
+ = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do
+ Status Badge
+ .badge-codes-block.bs-callout.bs-callout-info.hide
+ %p
+ Status badge for
+ %span.label.label-info #{@ref}
+ branch
+ %div
+ %label Markdown:
+ = text_field_tag 'badge_md', markdown_badge_code(@ci_project, @repository.root_ref), readonly: true, class: 'form-control'
+ %label Html:
+ = text_field_tag 'badge_html', html_badge_code(@ci_project, @repository.root_ref), readonly: true, class: 'form-control'
+
+= nested_form_for @ci_project, url: namespace_project_ci_settings_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f|
+ - if @ci_project.errors.any?
#error_explanation
- %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:"
+ %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:"
.alert.alert-error
%ul
- - @project.errors.full_messages.each do |msg|
+ - @ci_project.errors.full_messages.each do |msg|
%li= msg
%fieldset
@@ -83,9 +102,10 @@
%code \(\d+.\d+\%\) covered
%li
pytest-cov (Python) -
- %code \d+\%$
-
-
+ %code \d+\%\s*$
+ %li
+ phpunit --coverage-text --colors=never (PHP) -
+ %code ^\s*Lines:\s*\d+.\d+\%
%fieldset
%legend Advanced settings
@@ -93,8 +113,8 @@
= f.label :token, "CI token", class: 'control-label'
.col-sm-10
= f.text_field :token, class: 'form-control', placeholder: 'xEeFCaDAB89'
-
+
.form-actions
= f.submit 'Save changes', class: 'btn btn-save'
- - unless @project.new_record?
- = link_to 'Remove Project', ci_project_path(@project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right'
+ - unless @ci_project.new_record?
+ = link_to 'Remove Project', ci_project_path(@ci_project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right'
diff --git a/app/views/ci/projects/_no_runners.html.haml b/app/views/projects/ci_settings/_no_runners.html.haml
index c0a296fb17d..1374e6680f9 100644
--- a/app/views/ci/projects/_no_runners.html.haml
+++ b/app/views/projects/ci_settings/_no_runners.html.haml
@@ -4,5 +4,5 @@
%br
You can add Specific runner for this project on Runners page
- - if current_user.is_admin
- or add Shared runner for whole application in admin are.
+ - if current_user.admin
+ or add Shared runner for whole application in admin area.
diff --git a/app/views/projects/ci_settings/edit.html.haml b/app/views/projects/ci_settings/edit.html.haml
new file mode 100644
index 00000000000..acc912d4596
--- /dev/null
+++ b/app/views/projects/ci_settings/edit.html.haml
@@ -0,0 +1,6 @@
+- page_title "CI Settings"
+
+- if no_runners_for_project?(@ci_project)
+ = render 'no_runners'
+
+= render 'form'
diff --git a/app/views/ci/web_hooks/index.html.haml b/app/views/projects/ci_web_hooks/index.html.haml
index 78e8203b25e..2998fb08ff1 100644
--- a/app/views/ci/web_hooks/index.html.haml
+++ b/app/views/projects/ci_web_hooks/index.html.haml
@@ -1,12 +1,13 @@
+- page_title "CI Web Hooks"
%h3.page-title
- Web hooks
+ CI Web hooks
%p.light
Web Hooks can be used for binding events when build completed.
%hr.clearfix
-= form_for [:ci, @project, @web_hook], html: { class: 'form-horizontal' } do |f|
+= form_for @web_hook, url: namespace_project_ci_web_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f|
-if @web_hook.errors.any?
.alert.alert-danger
- @web_hook.errors.full_messages.each do |msg|
@@ -20,17 +21,18 @@
-if @web_hooks.any?
%h4 Activated web hooks (#{@web_hooks.count})
- %table.table
- - @web_hooks.each do |hook|
- %tr
- %td
- .clearfix
- %span.monospace= hook.url
- %td
- .pull-right
- - if @project.commits.any?
- = link_to 'Test Hook', test_ci_project_web_hook_path(@project, hook), class: "btn btn-sm btn-grouped"
- = link_to 'Remove', ci_project_web_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
+ .table-holder
+ %table.table
+ - @web_hooks.each do |hook|
+ %tr
+ %td
+ .clearfix
+ %span.monospace= hook.url
+ %td
+ .pull-right
+ - if @ci_project.commits.any?
+ = link_to 'Test Hook', test_namespace_project_ci_web_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped"
+ = link_to 'Remove', namespace_project_ci_web_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
%h4 Web Hook data example
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
new file mode 100644
index 00000000000..76dc87a8824
--- /dev/null
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -0,0 +1,9 @@
+%ul.center-top-menu.no-top.no-bottom.commit-ci-menu
+ = nav_link(path: 'commit#show') do
+ = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
+ Changes
+ %span.badge= @diffs.count
+ = nav_link(path: 'commit#builds') do
+ = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
+ Builds
+ %span.badge= @builds.count(:id)
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index fbf0a9ec0c3..d8bfe6a07ac 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -13,13 +13,14 @@
- unless @commit.parents.length > 1
%li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch)
%li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff)
- = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-primary btn-grouped" do
- %span Browse Code »
+ = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped" do
+ = icon('files-o')
+ Browse Files
%div
%p
%span.light Commit
- = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit)
+ = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
.commit-info-row
%span.light Authored by
%strong
@@ -36,7 +37,7 @@
.commit-info-row
%span.cgray= pluralize(@commit.parents.count, "parent")
- @commit.parents.each do |parent|
- = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent)
+ = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace"
- if @ci_commit
.pull-right
@@ -55,5 +56,5 @@
%pre.commit-description
= preserve(gfm(escape_once(@commit.description)))
-:coffeescript
- $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}")
+:javascript
+ $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml
new file mode 100644
index 00000000000..00cf9c76102
--- /dev/null
+++ b/app/views/projects/commit/builds.html.haml
@@ -0,0 +1,72 @@
+- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits"
+= render "projects/commits/header_title"
+= render "commit_box"
+= render "ci_menu"
+
+
+- if @ci_commit.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - @ci_commit.yaml_errors.split(",").each do |error|
+ %li= error
+
+- unless @ci_commit.ci_yaml_file
+ .bs-callout.bs-callout-warning
+ \.gitlab-ci.yml not found in this commit
+
+.gray-content-block.second-block
+ Latest builds
+
+ .pull-right
+ - if @ci_commit.duration > 0
+ %i.fa.fa-time
+ #{time_interval_in_words @ci_commit.duration}
+
+ &nbsp;
+
+ - if @ci_project && current_user && can?(current_user, :manage_builds, @project)
+ - if @ci_commit.builds.latest.failed.any?(&:retryable?)
+ = link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-primary', method: :post
+
+ - if @ci_commit.builds.running_or_pending.any?
+ = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-danger', method: :post
+
+.table-holder
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Ref
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @ci_project && @ci_project.coverage_enabled?
+ %th Coverage
+ %th
+ - @ci_commit.refs.each do |ref|
+ = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered,
+ locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true, allow_retry: true }
+
+- if @ci_commit.retried.any?
+ .gray-content-block.second-block
+ Retried builds
+
+ .table-holder
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Ref
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @ci_project && @ci_project.coverage_enabled?
+ %th Coverage
+ %th
+ = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried,
+ locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true }
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index f8681024d1b..069b8b1f169 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -1,5 +1,9 @@
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
= render "projects/commits/header_title"
= render "commit_box"
+- if @ci_commit
+ = render "ci_menu"
+- else
+ %div.block-connector
= render "projects/diffs/diffs", diffs: @diffs, project: @project
-= render "projects/notes/notes_with_form", view: params[:view]
+= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml
new file mode 100644
index 00000000000..9a0e7bff3f1
--- /dev/null
+++ b/app/views/projects/commit_statuses/_commit_status.html.haml
@@ -0,0 +1,74 @@
+%tr.commit_status
+ %td.status
+ = ci_status_with_icon(commit_status.status)
+
+ %td.commit_status-link
+ - if commit_status.target_url
+ = link_to commit_status.target_url do
+ %strong Build ##{commit_status.id}
+ - else
+ %strong Build ##{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(@project.namespace, @project, commit_status.sha), class: "monospace"
+
+ %td
+ - if commit_status.ref
+ = link_to commit_status.ref, namespace_project_commits_path(@project.namespace, @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, @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.gl_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 efad4cb1473..2e489a0a4d5 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -5,7 +5,7 @@
- note_count = notes.user.count
- ci_commit = project.ci_commit(commit.sha)
-- cache_key = [project.id, commit.id, note_count]
+- cache_key = [project.path_with_namespace, commit.id, note_count]
- cache_key.push(ci_commit.status) if ci_commit
= cache(cache_key) do
@@ -18,9 +18,9 @@
.pull-right
- if ci_commit
- = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}" do
- = ci_status_icon(ci_commit)
+ = render_ci_status(ci_commit)
&nbsp;
+ = clipboard_button(clipboard_text: commit.id)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
.notes_count
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index a849bf84698..f11a41cfd7b 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -12,7 +12,7 @@
Branches
%span.badge.js-totalbranch-count= @repository.branches.size
- = nav_link(controller: :tags) do
+ = nav_link(controller: [:tags, :releases]) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
Tags
%span.badge.js-totaltags-count= @repository.tags.length
diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder
index 3854ad5d611..268b9b815ee 100644
--- a/app/views/projects/commits/show.atom.builder
+++ b/app/views/projects/commits/show.atom.builder
@@ -12,7 +12,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id)
xml.title truncate(commit.title, length: 80)
xml.updated commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")
- xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(commit.author_email)
+ xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email))
xml.author do |author|
xml.name commit.author_name
xml.email commit.author_email
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 39755efd2fd..51088a7dea8 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -7,11 +7,11 @@
= render "form"
- if @commits.present?
- .prepend-top-20
+ .prepend-top-default
= render "projects/commits/commit_list"
= render "projects/diffs/diffs", diffs: @diffs, project: @project
- else
- .light-well.prepend-top-20
+ .light-well.prepend-top-default
.center
%h4
There isn't anything to compare.
diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml
index 91675b3738e..5e182af2669 100644
--- a/app/views/projects/deploy_keys/_form.html.haml
+++ b/app/views/projects/deploy_keys/_form.html.haml
@@ -1,5 +1,5 @@
%div
- = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal' } do |f|
+ = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal js-requires-input' } do |f|
-if @key.errors.any?
.alert.alert-danger
%ul
@@ -8,16 +8,15 @@
.form-group
= f.label :title, class: "control-label"
- .col-sm-10= f.text_field :title, class: 'form-control'
+ .col-sm-10= f.text_field :title, class: 'form-control', autofocus: true, required: true
.form-group
= f.label :key, class: "control-label"
.col-sm-10
%p.light
Paste a machine public key here. Read more about how to generate it
= link_to "here", help_page_path("ssh", "README")
- = f.text_area :key, class: "form-control thin_area", rows: 5
+ = f.text_area :key, class: "form-control thin_area", rows: 5, required: true
.form-actions
- = f.submit 'Create', class: "btn-create btn"
+ = f.submit 'Create Deploy Key', class: "btn-create btn"
= link_to "Cancel", namespace_project_deploy_keys_path(@project.namespace, @project), class: "btn btn-cancel"
-
diff --git a/app/views/projects/deploy_keys/new.html.haml b/app/views/projects/deploy_keys/new.html.haml
index 01c810aee18..01fab3008a7 100644
--- a/app/views/projects/deploy_keys/new.html.haml
+++ b/app/views/projects/deploy_keys/new.html.haml
@@ -1,5 +1,5 @@
- page_title "New Deploy Key"
-%h3.page-title New Deploy key
+%h3.page-title New Deploy Key
%hr
= render 'form'
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 2f24dc7c909..f9d661d59d2 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,21 +1,26 @@
-- if params[:view] == 'parallel'
+- if diff_view == 'parallel'
- fluid_layout true
-.gray-content-block.second-block
+- diff_files = safe_diff_files(diffs)
+
+.gray-content-block.middle-block.oneline-block
.inline-parallel-buttons
.btn-group
= inline_diff_btn
= parallel_diff_btn
- = render 'projects/diffs/stats', diffs: diffs
-
-- diff_files = safe_diff_files(diffs)
+ = 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
.files
- diff_files.each_with_index do |diff_file, index|
- = render 'projects/diffs/file', diff_file: diff_file, i: index, project: project
+ - diff_commit = commit_for_diff(diff_file)
+ - blob = project.repository.blob_for_diff(diff_commit, diff_file)
+ - next unless blob
+
+ = render 'projects/diffs/file', i: index, project: project,
+ diff_file: diff_file, diff_commit: diff_commit, blob: blob
- if @diff_timeout
.alert.alert-danger
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 99ee23a1ddc..b77e9f9f403 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,51 +1,51 @@
-- blob = project.repository.blob_for_diff(@commit, diff_file.diff)
-- return unless blob
-- blob_diff_path = namespace_project_blob_diff_path(project.namespace, project, tree_join(@commit.id, diff_file.file_path))
-.diff-file{id: "diff-#{i}", data: {blob_diff_path: blob_diff_path }}
- .diff-header{id: "file-path-#{hexdigest(diff_file.new_path || diff_file.old_path)}"}
- - if diff_file.deleted_file
- %span="#{diff_file.old_path} deleted"
-
- .diff-btn-group
- - if @commit.parent_ids.present?
- = view_file_btn(@commit.parent_id, diff_file, project)
- - elsif diff_file.diff.submodule?
+.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)}"}
+ - if diff_file.diff.submodule?
%span
- - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path)
- = submodule_link(submodule_item, @commit.id, project.repository)
+ = icon('archive fw')
+ %strong
+ = submodule_link(blob, @commit.id, project.repository)
- else
%span
- - if diff_file.renamed_file
- = "#{diff_file.old_path} renamed to #{diff_file.new_path}"
- - else
- = diff_file.new_path
+ = blob_icon blob.mode, blob.name
+ = link_to "#diff-#{i}" do
+ %strong
+ = diff_file.new_path
+
+ - if diff_file.deleted_file
+ deleted
+ - elsif diff_file.renamed_file
+ renamed from
+ %strong
+ = diff_file.old_path
+
- if diff_file.mode_changed?
- %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
+ %small
+ = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
- .diff-btn-group
+ .diff-controls
- if blob.text?
= link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do
%i.fa.fa-comments
&nbsp;
- - if @merge_request && @merge_request.source_project
+ - if editable_diff?(diff_file)
= edit_blob_link(@merge_request.source_project,
@merge_request.source_branch, diff_file.new_path,
after: '&nbsp;', from_merge_request_id: @merge_request.id)
- = view_file_btn(@commit.id, diff_file, project)
+ = view_file_btn(diff_commit.id, diff_file, project)
.diff-content.diff-wrap-lines
-# Skipp all non non-supported blobs
- return unless blob.respond_to?('text?')
- if blob.text?
- - if params[:view] == 'parallel'
+ - if 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(@commit, diff_file)
+ - 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/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index c4d7f26430b..ea2a3e01277 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -2,37 +2,35 @@
.commit-stat-summary
Showing
= link_to '#', class: 'js-toggle-button' do
- %strong #{pluralize(diffs.count, "changed file")}
- - if current_controller?(:commit)
- - unless @commit.has_zero_stats?
- with
- %strong.cgreen #{@commit.stats.additions} additions
- and
- %strong.cred #{@commit.stats.deletions} deletions
+ %strong #{pluralize(diff_files.count, "changed file")}
+ with
+ %strong.cgreen #{diff_files.sum(&:added_lines)} additions
+ and
+ %strong.cred #{diff_files.sum(&:removed_lines)} deletions
.file-stats.js-toggle-content.hide
%ul
- - diffs.each_with_index do |diff, i|
+ - diff_files.each_with_index do |diff_file, i|
%li
- - if diff.deleted_file
+ - if diff_file.deleted_file
%span.deleted-file
%a{href: "#diff-#{i}"}
%i.fa.fa-minus
- = diff.old_path
- - elsif diff.renamed_file
+ = diff_file.old_path
+ - elsif diff_file.renamed_file
%span.renamed-file
%a{href: "#diff-#{i}"}
%i.fa.fa-minus
- = diff.old_path
+ = diff_file.old_path
&rarr;
- = diff.new_path
- - elsif diff.new_file
+ = diff_file.new_path
+ - elsif diff_file.new_file
%span.new-file
%a{href: "#diff-#{i}"}
%i.fa.fa-plus
- = diff.new_path
+ = diff_file.new_path
- else
%span.edit-file
%a{href: "#diff-#{i}"}
%i.fa.fa-adjust
- = diff.new_path
+ = diff_file.new_path
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 90dce739992..b28ada909b7 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -14,7 +14,7 @@
= f.label :name, class: 'control-label' do
Project name
.col-sm-10
- = f.text_field :name, placeholder: "Example Project", class: "form-control", id: "project_name_edit"
+ = f.text_field :name, class: "form-control", id: "project_name_edit"
.form-group
@@ -22,7 +22,7 @@
Project description
%span.light (optional)
.col-sm-10
- = f.text_area :description, placeholder: "Awesome project", class: "form-control", rows: 3, maxlength: 250
+ = f.text_area :description, class: "form-control", rows: 3, maxlength: 250
- if @project.repository.exists? && @project.repository.branch_names.any?
.form-group
@@ -35,7 +35,7 @@
.form-group
= f.label :tag_list, "Tags", class: 'control-label'
.col-sm-10
- = f.text_field :tag_list, maxlength: 2000, class: "form-control"
+ = f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control"
%p.help-block Separate tags with commas.
%fieldset.features
@@ -57,7 +57,16 @@
= f.check_box :merge_requests_enabled
%strong Merge Requests
%br
- %span.descr Submit changes to be merged upstream.
+ %span.descr Submit changes to be merged upstream
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :builds_enabled do
+ = f.check_box :builds_enabled
+ %strong Builds
+ %br
+ %span.descr Test and deploy your changes before merge
.form-group
.col-sm-offset-2.col-sm-10
@@ -121,9 +130,11 @@
The project can be committed to.
%br
%strong Once active this project shows up in the search and on the dashboard.
- = link_to 'Unarchive', unarchive_namespace_project_path(@project.namespace, @project),
- data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." },
- method: :post, class: "btn btn-success"
+
+ .form-actions
+ = link_to 'Unarchive project', unarchive_namespace_project_path(@project.namespace, @project),
+ data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." },
+ method: :post, class: "btn btn-success"
- else
.panel.panel-warning
.panel-heading
@@ -135,9 +146,11 @@
It is hidden from the dashboard and doesn't show up in searches.
%br
%strong Archived projects cannot be committed to!
- = link_to 'Archive', archive_namespace_project_path(@project.namespace, @project),
- data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." },
- method: :post, class: "btn btn-warning"
+
+ .form-actions
+ = link_to 'Archive project', archive_namespace_project_path(@project.namespace, @project),
+ data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." },
+ method: :post, class: "btn btn-warning"
- else
.nothing-here-block Only the project owner can archive a project
@@ -151,7 +164,7 @@
Project name
.col-sm-9
.form-group
- = f.text_field :name, placeholder: "Example Project", class: "form-control"
+ = f.text_field :name, class: "form-control"
.form-group
= f.label :path, class: 'control-label' do
%span Path
@@ -161,12 +174,11 @@
.input-group-addon
#{URI.join(root_url, @project.namespace.path)}/
= f.text_field :path, class: 'form-control'
- %span.input-group-addon .git
%ul
%li Be careful. Renaming a project's repository can have unintended side effects.
%li You will need to update your local repositories to point to the new location.
.form-actions
- = f.submit 'Rename', class: "btn btn-warning"
+ = f.submit 'Rename project', class: "btn btn-warning"
- if can?(current_user, :change_namespace, @project)
.panel.panel-default.panel.panel-danger
@@ -185,23 +197,40 @@
%li You can only transfer the project to namespaces you manage.
%li You will need to update your local repositories to point to the new location.
.form-actions
- = f.submit 'Transfer', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
+ = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
- else
.nothing-here-block Only the project owner can transfer a project
+ - if @project.forked?
+ - if can?(current_user, :remove_fork_project, @project)
+ = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f|
+ .panel.panel-default.panel.panel-danger
+ .panel-heading Remove fork relationship
+ .panel-body
+ %p
+ This will remove the fork relationship to source project
+ #{link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)}.
+ %br
+ %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.
+ .form-actions
+ = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) }
+ - else
+ .nothing-here-block Only the project owner can remove the fork relationship.
+
- if can?(current_user, :remove_project, @project)
.panel.panel-default.panel.panel-danger
.panel-heading Remove project
.panel-body
- = form_tag(namespace_project_path(@project.namespace, @project), method: :delete, html: { class: 'form-horizontal'}) do
+ = form_tag(namespace_project_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do
%p
Removing the project will delete its repository and all related resources including issues, merge requests etc.
%br
%strong Removed projects cannot be restored!
-
- = link_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
+ .form-actions
+ = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
- else
- .nothing-here-block Only project owner can remove a project
+ .nothing-here-block Only the project owner can remove a project.
+
.save-project-loader.hide
.center
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 185ebf23934..950ab33825e 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,52 +1,56 @@
-- if current_user && can?(current_user, :download_code, @project)
- = render 'shared/no_ssh'
- = render 'shared/no_password'
+.alert_holder
+ - if current_user && can?(current_user, :download_code, @project)
+ = render 'shared/no_ssh'
+ = render 'shared/no_password'
= render "home_panel"
-.gray-content-block.center
+.gray-content-block.second-block.center
%h3.page-title
The repository for this project is empty
- %p
- If you already have files you can push them using command line instructions below.
- %br
- Otherwise you can start with
- = link_to "adding README", new_readme_path, class: 'underlined-link'
- file to this project.
+ - if can?(current_user, :push_code, @project)
+ %p
+ If you already have files you can push them using command line instructions below.
+ %p
+ Otherwise you can start with
+ = link_to "adding README", new_readme_path, class: 'underlined-link'
+ file to this project.
-.prepend-top-20
-%h3.page-title
- Command line instructions
-%div.git-empty
- %fieldset
- %h5 Git global setup
- %pre.light-well
- :preserve
- git config --global user.name "#{h git_user_name}"
- git config --global user.email "#{h git_user_email}"
+- if can?(current_user, :download_code, @project)
+ .prepend-top-20
+ .empty_wrapper
+ %h3.page-title-empty
+ Command line instructions
+ %div.git-empty
+ %fieldset
+ %h5 Git global setup
+ %pre.light-well
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
- %fieldset
- %h5 Create a new repository
- %pre.light-well
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- cd #{h @project.path}
- touch README.md
- git add README.md
- git commit -m "add README"
- git push -u origin master
+ %fieldset
+ %h5 Create a new repository
+ %pre.light-well
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ cd #{h @project.path}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ git push -u origin master
- %fieldset
- %h5 Existing folder or Git repository
- %pre.light-well
- :preserve
- cd existing_folder
- git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- git add .
- git commit
- git push -u origin master
+ %fieldset
+ %h5 Existing folder or Git repository
+ %pre.light-well
+ :preserve
+ cd existing_folder
+ git init
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git add .
+ git commit
+ git push -u origin master
-- if can? current_user, :remove_project, @project
- .prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+ - if can? current_user, :remove_project, @project
+ .prepend-top-20
+ = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index cd5f3a5d39e..f0b0a11c04a 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -1,36 +1,41 @@
- page_title "Fork project"
-%h3.page-title Fork project
-%p.lead
- Click to fork the project to a user or group
-%hr
+- if @namespaces.present?
+ %h3.page-title Fork project
+ %p.lead
+ Click to fork the project to a user or group
+ %hr
-.fork-namespaces
- - @namespaces.in_groups_of(6, false) do |group|
- .row
- - group.each do |namespace|
- .col-md-2.col-sm-3
- - if fork = namespace.find_fork_of(@project)
- .fork-thumbnail
- = link_to project_path(fork), title: "Visit project fork", class: 'has_tooltip' do
- = image_tag namespace_icon(namespace, 100)
- .caption
- %strong
- = namespace.human_name
- %div.text-primary
- Already forked
+ .fork-namespaces
+ - @namespaces.in_groups_of(6, false) do |group|
+ .row
+ - group.each do |namespace|
+ .col-md-2.col-sm-3
+ - if fork = namespace.find_fork_of(@project)
+ .fork-thumbnail
+ = link_to project_path(fork), title: "Visit project fork", class: 'has_tooltip' do
+ = image_tag namespace_icon(namespace, 100)
+ .caption
+ %strong
+ = namespace.human_name
+ %div.text-primary
+ Already forked
- - 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
- = image_tag namespace_icon(namespace, 100)
- .caption
- %strong
- = namespace.human_name
+ - 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
+ = image_tag namespace_icon(namespace, 100)
+ .caption
+ %strong
+ = namespace.human_name
- %p.light
- Fork is a copy of a project repository.
- %br
- Forking a repository allows you to do changes without affecting the original project.
+ %p.light
+ Fork is a copy of a project repository.
+ %br
+ Forking a repository allows you to do changes without affecting the original project.
+- else
+ %h3 No available namespaces to fork the project
+ %p.slead
+ You must have permission to create a project in a namespace before forking.
.save-project-loader.hide
.center
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index bbfaf422a82..03d0733f913 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,9 +1,9 @@
-%ul.nav.nav-tabs
+%ul.center-top-menu
= nav_link(action: :show) do
= link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do
= link_to 'Commits', commits_namespace_project_graph_path
- - if @project.gitlab_ci?
+ - if @project.builds_enabled?
= nav_link(action: :ci) do
= link_to ci_namespace_project_graph_path do
Continuous Integration
diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml
index 4f69cc64f7c..6fa77cc10c6 100644
--- a/app/views/projects/graphs/ci.html.haml
+++ b/app/views/projects/graphs/ci.html.haml
@@ -1,7 +1,16 @@
- page_title "Continuous Integration", "Graphs"
= render "header_title"
= render 'head'
+.gray-content-block.append-bottom-default
+ .oneline
+ A collection of graphs for Continuous Integration
+
#charts.ci-charts
+ .row
+ .col-md-6
+ = render 'projects/graphs/ci/overall'
+ .col-md-6
+ = render 'projects/graphs/ci/build_times'
+
+ %hr
= render 'projects/graphs/ci/builds'
- = render 'projects/graphs/ci/build_times'
-= render 'projects/graphs/ci/overall'
diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml
index c3c2f572414..c58223fd39e 100644
--- a/app/views/projects/graphs/ci/_build_times.haml
+++ b/app/views/projects/graphs/ci/_build_times.haml
@@ -1,21 +1,22 @@
-%fieldset
- %legend
+%div
+ %p.light
Commit duration in minutes for last 30 commits
- %canvas#build_timesChart.padded{width: 800, height: 300}
+ %canvas#build_timesChart{height: 200}
:javascript
var data = {
labels : #{@charts[:build_times].labels.to_json},
datasets : [
{
- fillColor : "#4A3",
- strokeColor : "rgba(151,187,205,1)",
- pointColor : "rgba(151,187,205,1)",
- pointStrokeColor : "#fff",
+ fillColor : "rgba(220,220,220,0.5)",
+ strokeColor : "rgba(220,220,220,1)",
+ barStrokeWidth: 1,
+ barValueSpacing: 1,
+ barDatasetSpacing: 1,
data : #{@charts[:build_times].build_times.to_json}
}
]
}
var ctx = $("#build_timesChart").get(0).getContext("2d");
- new Chart(ctx).Line(data,{"scaleOverlay": true});
+ new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false});
diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml
index 1b0039fb834..8fca07114fa 100644
--- a/app/views/projects/graphs/ci/_builds.haml
+++ b/app/views/projects/graphs/ci/_builds.haml
@@ -1,20 +1,30 @@
-%fieldset
- %legend
- Builds chart for last week
- (#{date_from_to(Date.today - 7.days, Date.today)})
+%h4 Build charts
+%p
+ &nbsp;
+ %span.cgreen
+ = icon("circle")
+ success
+ &nbsp;
+ %span.cgray
+ = icon("circle")
+ all
- %canvas#weekChart.padded{width: 800, height: 200}
+.prepend-top-default
+ %p.light
+ Builds for last week
+ (#{date_from_to(Date.today - 7.days, Date.today)})
+ %canvas#weekChart{height: 200}
-%fieldset
- %legend
- Builds chart for last month
+.prepend-top-default
+ %p.light
+ Builds for last month
(#{date_from_to(Date.today - 30.days, Date.today)})
+ %canvas#monthChart{height: 200}
- %canvas#monthChart.padded{width: 800, height: 300}
-
-%fieldset
- %legend Builds chart for last year
- %canvas#yearChart.padded{width: 800, height: 400}
+.prepend-top-default
+ %p.light
+ Builds for last year
+ %canvas#yearChart.padded{height: 250}
- [:week, :month, :year].each do |scope|
:javascript
@@ -22,20 +32,20 @@
labels : #{@charts[scope].labels.to_json},
datasets : [
{
- fillColor : "rgba(220,220,220,0.5)",
- strokeColor : "rgba(220,220,220,1)",
- pointColor : "rgba(220,220,220,1)",
+ fillColor : "#7f8fa4",
+ strokeColor : "#7f8fa4",
+ pointColor : "#7f8fa4",
pointStrokeColor : "#EEE",
data : #{@charts[scope].total.to_json}
},
{
- fillColor : "#4A3",
- strokeColor : "rgba(151,187,205,1)",
- pointColor : "rgba(151,187,205,1)",
+ fillColor : "#44aa22",
+ strokeColor : "#44aa22",
+ pointColor : "#44aa22",
pointStrokeColor : "#fff",
data : #{@charts[scope].success.to_json}
}
]
}
var ctx = $("##{scope}Chart").get(0).getContext("2d");
- new Chart(ctx).Line(data,{"scaleOverlay": true});
+ new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false});
diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/graphs/ci/_overall.haml
index 9550d719471..cf4285a2671 100644
--- a/app/views/projects/graphs/ci/_overall.haml
+++ b/app/views/projects/graphs/ci/_overall.haml
@@ -1,22 +1,20 @@
- ci_project = @project.gitlab_ci_project
-%fieldset
- %legend Overall
- %p
+%h4 Overall stats
+%ul
+ %li
Total:
%strong= pluralize ci_project.builds.count(:all), 'build'
- %p
+ %li
Successful:
%strong= pluralize ci_project.builds.success.count(:all), 'build'
- %p
+ %li
Failed:
%strong= pluralize ci_project.builds.failed.count(:all), 'build'
-
- %p
+ %li
Success ratio:
%strong
#{success_ratio(ci_project.builds.success, ci_project.builds.failed)}%
-
- %p
+ %li
Commits covered:
%strong
= ci_project.commits.count(:all)
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml
index 112be875b6b..fc465ab273b 100644
--- a/app/views/projects/graphs/commits.html.haml
+++ b/app/views/projects/graphs/commits.html.haml
@@ -1,9 +1,13 @@
- page_title "Commits", "Graphs"
= render "header_title"
-.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'graphs_commits'
= render 'head'
+.gray-content-block.append-bottom-default
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'graphs_commits'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
+
%p.lead
Commit statistics for
%strong #{@ref}
@@ -45,26 +49,24 @@
Commits per weekday
%canvas#weekday-chart
-:coffeescript
- responsiveChart = (selector, data) ->
- options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false }
-
- # get selector by context
- ctx = selector.get(0).getContext("2d")
- # pointing parent container to make chart.js inherit its width
- container = $(selector).parent()
-
- generateChart = ->
- selector.attr('width', $(container).width())
- new Chart(ctx).Bar(data, options)
-
- # enabling auto-resizing
- $(window).resize( generateChart )
-
- generateChart()
+:javascript
+ var responsiveChart = function (selector, data) {
+ var options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false };
+ // get selector by context
+ var ctx = selector.get(0).getContext("2d");
+ // pointing parent container to make chart.js inherit its width
+ var container = $(selector).parent();
+ var generateChart = function() {
+ selector.attr('width', $(container).width());
+ return new Chart(ctx).Bar(data, options);
+ };
+ // enabling auto-resizing
+ $(window).resize(generateChart);
+ return generateChart();
+ };
- chartData = (keys, values) ->
- data = {
+ var chartData = function (keys, values) {
+ var data = {
labels : keys,
datasets : [{
fillColor : "rgba(220,220,220,0.5)",
@@ -74,13 +76,15 @@
barDatasetSpacing: 1,
data : values
}]
- }
+ };
+ return data;
+ };
- hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json})
- responsiveChart($('#hour-chart'), hourData)
+ var hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json});
+ responsiveChart($('#hour-chart'), hourData);
- dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json})
- responsiveChart($('#weekday-chart'), dayData)
+ var dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json});
+ responsiveChart($('#weekday-chart'), dayData);
- monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json})
- responsiveChart($('#month-chart'), monthData)
+ var monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json});
+ responsiveChart($('#month-chart'), monthData);
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index bd342911e49..882e7d6b6ee 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,9 +1,13 @@
- page_title "Contributors", "Graphs"
= render "header_title"
-.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'graphs'
= render 'head'
+.gray-content-block.append-bottom-default
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'graphs'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
+
.loading-graph
.center
%h3.page-title
@@ -24,18 +28,21 @@
-:coffeescript
- $.ajax
+:javascript
+ $.ajax({
type: "GET",
url: location.href,
- success: (data) ->
- graph = new ContributorsStatGraph()
- graph.init(data)
+ dataType: "json",
+ success: function (data) {
+ var graph = new ContributorsStatGraph();
+ graph.init(data);
- $("#brush_change").change ->
- graph.change_date_header()
- graph.redraw_authors()
+ $("#brush_change").change(function(){
+ graph.change_date_header();
+ graph.redraw_authors();
+ });
$(".stat-graph").fadeIn();
$(".loading-graph").hide();
- dataType: "json"
+ }
+ });
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index 85dbfd67862..3702aeaecba 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -19,7 +19,7 @@
= f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json'
.form-group
= f.label :url, "Trigger", class: 'control-label'
- .col-sm-10
+ .col-sm-10.prepend-top-10
%div
= f.check_box :push_events, class: 'pull-left'
.prepend-left-20
diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml
index f8f2e192e29..6027fb23360 100644
--- a/app/views/projects/imports/new.html.haml
+++ b/app/views/projects/imports/new.html.haml
@@ -1,22 +1,19 @@
- page_title "Import repository"
%h3.page-title
- - if @project.import_failed?
- Import failed. Retry?
- - else
- Import repository
+ Import repository
%hr
+- if @project.import_failed?
+ .panel.panel-danger
+ .panel-heading The repository could not be imported.
+ .panel-body
+ %pre
+ :preserve
+ #{@project.import_error.try(:strip)}
+
= form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f|
- .form-group.import-url-data
- = f.label :import_url, class: 'control-label' do
- %span Import existing git repo
- .col-sm-10
- = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git'
- .well.prepend-top-20
- This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git.
- %br
- The import will time out after 4 minutes. For big repositories, use a clone/push combination.
- For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"}
+ = render "shared/import_form", f: f
+
.form-actions
= f.submit 'Start import', class: "btn btn-create", tabindex: 4
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 06886d215a3..c0d1ce0d120 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -8,7 +8,7 @@
- else
Import in progress.
- unless @project.forked?
- %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)}
+ %p.monospace git clone --bare #{@project.safe_import_url}
%p Please wait while we import the repository for you. Refresh at will.
:javascript
new ProjectImport();
diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml
new file mode 100644
index 00000000000..917d5181689
--- /dev/null
+++ b/app/views/projects/issues/_closed_by_box.html.haml
@@ -0,0 +1,3 @@
+.issue-closed-by-widget
+ = icon('check')
+ This issue will be closed automatically when merge request #{gfm(merge_requests_sentence(@closed_by_merge_requests))} is accepted.
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index d4a98eca473..b5f522f2079 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -7,7 +7,7 @@
= render 'shared/show_aside'
-.gray-content-block.second-block
+.gray-content-block.second-block.oneline-block
.row
.col-md-9
.votes-holder.pull-right
@@ -17,8 +17,10 @@
- @participants.each do |participant|
= link_to_member(@project, participant, name: false, size: 24)
.col-md-3
- %span.slead.has_tooltip{title: 'Cross-project reference'}
- = cross_project_reference(@project, @issue)
+ .input-group.cross-project-reference
+ %span#cross-project-reference.slead.has_tooltip{title: 'Cross-project reference'}
+ = cross_project_reference(@project, @issue)
+ = clipboard_button(clipboard_target: '#cross-project-reference')
.row
%section.col-md-9
@@ -27,10 +29,3 @@
.issuable-affix
.context
= render 'shared/issuable/context', issuable: @issue
-
- - if @issue.labels.any?
- .issuable-context-title
- %label Labels
- .issue-show-labels
- - @issue.labels.each do |label|
- = link_to_label(label)
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index f39bb7d2574..6588d9bdbe1 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,9 +1,5 @@
-%div.issue-form-holder
- %h3.page-title= @issue.new_record? ? "Create Issue" : "Edit Issue ##{@issue.iid}"
- %hr
-
- = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form' } do |f|
- = render 'shared/issuable/form', f: f, issuable: @issue
+= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-requires-input' } do |f|
+ = render 'shared/issuable/form', f: f, issuable: @issue
:javascript
$('.assign-to-me-link').on('click', function(e){
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 55ce912829d..1eb71990e55 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -6,37 +6,39 @@
.issue-title
%span.issue-title-text
= link_to_gfm issue.title, issue_path(issue), class: "row_title"
- .issue-labels
- - issue.labels.each do |label|
- = link_to_label(label, project: issue.project)
.pull-right.light
- if issue.closed?
%span
CLOSED
- if issue.assignee
- = link_to_member(@project, issue.assignee, name: false)
+ = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
- note_count = issue.notes.user.count
- if note_count > 0
&nbsp;
- %span
- %i.fa.fa-comments
+ = link_to issue_path(issue) + "#notes" do
+ = icon('comments')
= note_count
- else
&nbsp;
- %span.issue-no-comments
- %i.fa.fa-comments
+ = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do
+ = icon('comments')
= 0
.issue-info
- = "#{issue.to_reference} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe
- - if issue.votes_count > 0
- = render 'votes/votes_inline', votable: issue
+ #{issue.to_reference} &middot;
+ opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
+ by #{link_to_member(@project, issue.author, avatar: false)}
- if issue.milestone
&nbsp;
- %span
- %i.fa.fa-clock-o
+ = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
+ = icon('clock-o')
= issue.milestone.title
+ - if issue.labels.any?
+ &nbsp;
+ - issue.labels.each do |label|
+ = link_to_label(label, project: issue.project)
- if issue.tasks?
+ &nbsp;
%span.task-status
= issue.task_status
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index a3399c57aa2..ca5b1a8386d 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -5,8 +5,9 @@
.nothing-here-block No issues to show
- if @issues.present?
- .pull-right
- %span.issue_counter #{@issues.total_count}
- issues for this filter
+ .issuable-filter-count
+ %span.pull-right
+ = @issues.total_count
+ issues for this filter
= paginate @issues, theme: "gitlab"
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
index 53b6f0879c9..20216297d25 100644
--- a/app/views/projects/issues/edit.html.haml
+++ b/app/views/projects/issues/edit.html.haml
@@ -1,2 +1,8 @@
- page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues"
+= render "header_title"
+
+%h3.page-title
+ Edit Issue ##{@issue.iid}
+%hr
+
= render "form"
diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml
index 153447baa1b..b317a0c1cf4 100644
--- a/app/views/projects/issues/new.html.haml
+++ b/app/views/projects/issues/new.html.haml
@@ -1,4 +1,8 @@
- page_title "New Issue"
= render "header_title"
+%h3.page-title
+ New Issue
+%hr
+
= render "form"
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 5cb814c9ea8..e2de17cee6d 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -11,7 +11,8 @@
Open
%span.issue-id Issue ##{@issue.iid}
%span.creator
- &middot; created by #{link_to_member(@project, @issue.author, size: 24)}
+ &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
@@ -46,6 +47,7 @@
= markdown(@issue.description)
%textarea.hidden.js-task-list-field
= @issue.description
-
+ - if @closed_by_merge_requests.present?
+ = render 'projects/issues/closed_by_box'
.issue-discussion
= render 'projects/issues/discussion'
diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml
index 534c545329b..5ce2a7b985d 100644
--- a/app/views/projects/labels/_form.html.haml
+++ b/app/views/projects/labels/_form.html.haml
@@ -10,9 +10,9 @@
.form-group
= f.label :title, class: 'control-label'
.col-sm-10
- = f.text_field :title, class: "form-control", required: true
+ = f.text_field :title, class: "form-control js-quick-submit", required: true, autofocus: true
.form-group
- = f.label :color, "Background Color", class: 'control-label'
+ = f.label :color, "Background color", class: 'control-label'
.col-sm-10
.input-group
.input-group-addon.label-color-preview &nbsp;
@@ -28,6 +28,8 @@
&nbsp;
.form-actions
- = f.submit 'Save', class: 'btn btn-save js-save-button'
+ - if @label.persisted?
+ = f.submit 'Save changes', class: 'btn btn-save js-save-button'
+ - else
+ = f.submit 'Create Label', class: 'btn btn-create js-save-button'
= link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel'
-
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index c6ebfa281a1..b70a9fc9fe5 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -7,4 +7,4 @@
- if can? current_user, :admin_label, @project
= link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm'
- = link_to 'Remove', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
+ = link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
diff --git a/app/views/projects/labels/destroy.js.haml b/app/views/projects/labels/destroy.js.haml
index 1b4c83ab097..d59563b122a 100644
--- a/app/views/projects/labels/destroy.js.haml
+++ b/app/views/projects/labels/destroy.js.haml
@@ -1,2 +1,2 @@
- if @project.labels.size == 0
- $('.labels').load(document.URL + ' .light-well').hide().fadeIn(1000)
+ $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index bc4ab0ca27c..675a805e12f 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,11 +1,7 @@
- page_title "Edit", @label.name, "Labels"
= render "header_title"
-%h3
- Edit label
- %span.light #{@label.name}
-.back-link
- = link_to namespace_project_labels_path(@project.namespace, @project) do
- &larr; To labels list
+%h3.page-title
+ Edit Label
%hr
= render 'form'
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 97175f8232b..9081bcfe9b3 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -4,6 +4,7 @@
.gray-content-block.top-block
- if can? current_user, :admin_label, @project
= link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do
+ = icon('plus')
New label
.oneline
Labels can be applied to issues and merge requests.
@@ -14,8 +15,8 @@
= render @labels
= paginate @labels, theme: 'gitlab'
- else
- .light-well
+ .nothing-here-block
- if can? current_user, :admin_label, @project
- .nothing-here-block Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels
+ Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels
- else
- .nothing-here-block No labels created
+ No labels created
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index 342ad4f3f95..e20fd7d6891 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -1,9 +1,7 @@
- page_title "New Label"
= render "header_title"
-%h3 New label
-.back-link
- = link_to namespace_project_labels_path(@project.namespace, @project) do
- &larr; To labels list
+%h3.page-title
+ New Label
%hr
= render 'form'
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index 38e66c3828b..ea462561668 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -7,15 +7,17 @@
= render 'shared/show_aside'
-.gray-content-block.second-block
+.gray-content-block.middle-block.oneline-block
.row
.col-md-9
.votes-holder.pull-right
#votes= render 'votes/votes_block', votable: @merge_request
= render "projects/merge_requests/show/participants"
.col-md-3
- %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
- = cross_project_reference(@project, @merge_request)
+ .input-group.cross-project-reference
+ %span#cross-project-reference.slead.has_tooltip{title: 'Cross-project reference'}
+ = cross_project_reference(@project, @merge_request)
+ = clipboard_button(clipboard_target: '#cross-project-reference')
.row
%section.col-md-9
@@ -24,10 +26,3 @@
.issuable-affix
.context
= render 'shared/issuable/context', issuable: @merge_request
-
- - if @merge_request.labels.any?
- .issuable-context-title
- %label Labels
- .merge-request-show-labels
- - @merge_request.labels.each do |label|
- = link_to_label(label)
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index 9cf389dbe38..3e4ab09c6d4 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -1,6 +1,5 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f|
- .merge-request-form-info
- = render 'shared/issuable/form', f: f, issuable: @merge_request
+ = render 'shared/issuable/form', f: f, issuable: @merge_request
:javascript
$('.assign-to-me-link').on('click', function(e){
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 25e4e8ba80d..1d4c9b66c42 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,44 +1,54 @@
+- ci_commit = merge_request.ci_commit
%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"
- .merge-request-labels
- - merge_request.labels.each do |label|
- = link_to_label(label, project: merge_request.project)
.pull-right.light
+ - if ci_commit
+ = render_ci_status(ci_commit)
- if merge_request.merged?
%span
- %i.fa.fa-check
+ = icon('check')
MERGED
- elsif merge_request.closed?
%span
- %i.fa.fa-ban
+ = icon('ban')
CLOSED
- note_count = merge_request.mr_and_commit_notes.user.count
- if merge_request.assignee
&nbsp;
- = link_to_member(merge_request.source_project, merge_request.assignee, name: false)
+ = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
- if note_count > 0
&nbsp;
- %span
- %i.fa.fa-comments
+ = link_to merge_request_path(merge_request) + "#notes" do
+ = icon('comments')
= note_count
- else
&nbsp;
- %span.merge-request-no-comments
- %i.fa.fa-comments
+ = link_to merge_request_path(merge_request) + "#notes", class: "merge-request-no-comments" do
+ = icon('comments')
= 0
.merge-request-info
- = "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe
- - if merge_request.votes_count > 0
- = render 'votes/votes_inline', votable: merge_request
- - if merge_request.milestone_id?
+ \##{merge_request.iid} &middot;
+ opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
+ by #{link_to_member(@project, merge_request.author, avatar: false)}
+ - if merge_request.target_project.default_branch != merge_request.target_branch
+ &nbsp;
+ = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do
+ = icon('code-fork')
+ = merge_request.target_branch
+ - if merge_request.milestone
&nbsp;
- %span
- %i.fa.fa-clock-o
+ = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do
+ = icon('clock-o')
= merge_request.milestone.title
+ - if merge_request.labels.any?
+ &nbsp;
+ - merge_request.labels.each do |label|
+ = link_to_label(label, project: merge_request.project)
- if merge_request.tasks?
+ &nbsp;
%span.task-status
= merge_request.task_status
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index d86707b3d97..0af970e4b92 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -5,8 +5,10 @@
.nothing-here-block No merge requests to show
- if @merge_requests.present?
- .pull-right
- %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter
+ .issuable-filter-count
+ %span.pull-right
+ = @merge_requests.total_count
+ merge requests for this filter
= paginate @merge_requests, theme: "gitlab"
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 452006162db..236a545c840 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -1,4 +1,5 @@
-%p.lead Compare branches for new Merge Request
+%h3.page-title
+ New Merge Request
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: new_namespace_project_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
@@ -10,7 +11,7 @@
.panel-body
= f.select(:source_project_id, [[@merge_request.source_project_path,@merge_request.source_project.id]] , {}, { class: 'source_project select2 span3', disabled: @merge_request.persisted?, required: true })
&nbsp;
- = f.select(:source_branch, @merge_request.source_branches, { include_blank: "Select branch" }, {class: 'source_branch select2 span2', required: true})
+ = f.select(:source_branch, @merge_request.source_branches, { include_blank: true }, { class: 'source_branch select2 span2', required: true, data: { placeholder: "Select source branch" } })
.panel-footer
.mr_source_commit
@@ -22,7 +23,7 @@
- projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project]
= f.select(:target_project_id, options_from_collection_for_select(projects, 'id', 'path_with_namespace', f.object.target_project_id), {}, { class: 'target_project select2 span3', disabled: @merge_request.persisted?, required: true })
&nbsp;
- = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, {class: 'target_branch select2 span2', required: true})
+ = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', required: true, data: { placeholder: "Select target branch" } })
.panel-footer
.mr_target_commit
@@ -37,7 +38,7 @@
%h4 Compare failed
%p We can't compare selected branches. It may be because of huge diff. Please try again or select different branches.
- else
- .light-well.append-bottom-10
+ .light-well.append-bottom-default
.center
%h4
There isn't anything to merge.
@@ -51,8 +52,8 @@
are the same.
- %div
- = f.submit 'Compare branches', class: "btn btn-new mr-compare-btn"
+ .form-actions
+ = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn"
:javascript
var source_branch = $("#merge_request_source_branch")
@@ -77,12 +78,12 @@
});
-:coffeescript
-
- $(".merge-request-form").on 'submit', ->
- if $("#merge_request_source_branch").val() is "" or $('#merge_request_target_branch').val() is ""
- $(".mr-compare-errors").html("You must select source and target branch to proceed")
- $(".mr-compare-errors").fadeIn()
- event.preventDefault()
- return
-
+:javascript
+ $(".merge-request-form").on('submit', function () {
+ if ($("#merge_request_source_branch").val() === "" || $('#merge_request_target_branch').val() === "") {
+ $(".mr-compare-errors").html("You must select source and target branch to proceed");
+ $(".mr-compare-errors").fadeIn();
+ event.preventDefault();
+ return;
+ }
+ });
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 46aeecd8733..156922cea41 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -1,24 +1,24 @@
%h3.page-title
- New merge request
+ New Merge Request
%p.slead
+ - source_title, target_title = format_mr_branch_names(@merge_request)
From
- %strong.label-branch #{@merge_request.source_project_namespace}:#{@merge_request.source_branch}
+ %strong.label-branch #{source_title}
%span into
- %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch}
+ %strong.label-branch #{target_title}
%span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f|
- .merge-request-form-info
- = render 'shared/issuable/form', f: f, issuable: @merge_request
- = f.hidden_field :source_project_id
- = f.hidden_field :source_branch
- = f.hidden_field :target_project_id
- = f.hidden_field :target_branch
+ = render 'shared/issuable/form', f: f, issuable: @merge_request
+ = f.hidden_field :source_project_id
+ = f.hidden_field :source_branch
+ = f.hidden_field :target_project_id
+ = f.hidden_field :target_branch
.mr-compare.merge-request
- %ul.merge-request-tabs
+ %ul.merge-request-tabs.center-top-menu.no-top.no-bottom
%li.commits-tab
= link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do
Commits
@@ -30,7 +30,7 @@
.tab-content
#commits.commits.tab-pane
- = render "projects/commits/commits", project: @project
+ = render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane.active
- if @diffs.present?
= render "projects/diffs/diffs", diffs: @diffs, project: @project
@@ -56,4 +56,3 @@
diffs_loaded: true,
commits_loaded: true
});
-
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 0b0f52c653c..f5aff0877e7 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -8,7 +8,7 @@
.merge-request-details.issuable-details
= render "projects/merge_requests/show/mr_title"
= render "projects/merge_requests/show/mr_box"
- .append-bottom-20.mr-source-target.prepend-top-default
+ .append-bottom-default.mr-source-target.prepend-top-default
- if @merge_request.open?
.pull-right
- if @merge_request.source_branch_exists?
@@ -24,22 +24,25 @@
%ul.dropdown-menu
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
- .light
+ .normal
%span Request to merge
- %span.label-branch #{source_branch_with_namespace(@merge_request)}
+ %span.label-branch
+ = source_branch_with_namespace(@merge_request)
%span into
- %span.label-branch #{@merge_request.target_branch}
+ = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do
+ = @merge_request.target_branch
= render "projects/merge_requests/show/how_to_merge"
= render "projects/merge_requests/widget/show.html.haml"
- - if @merge_request.open? && @merge_request.can_be_merged?
- .light
+ - if @merge_request.open? && @merge_request.source_branch_exists? && @merge_request.can_be_merged? && @merge_request.can_be_merged_by?(current_user)
+ .light.prepend-top-default
You can also accept this merge request manually using the
- = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
+ = 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
+ %ul.merge-request-tabs.center-top-menu.no-top.no-bottom
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#notes', action: 'notes', toggle: 'tab'} do
Discussion
diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index 303ca0a880b..fc62bb5bce9 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -2,6 +2,6 @@
= render "header_title"
%h3.page-title
- = "Edit merge request ##{@merge_request.iid}"
+ Edit Merge Request ##{@merge_request.iid}
%hr
= render 'form'
diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml
index 33321651e32..518ecb9f00f 100644
--- a/app/views/projects/merge_requests/merge.js.haml
+++ b/app/views/projects/merge_requests/merge.js.haml
@@ -1,6 +1,6 @@
- if @status
:plain
- merge_request_widget.mergeInProgress();
+ merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'});
- else
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}");
diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml
index 9fdde80c6d9..d259968030e 100644
--- a/app/views/projects/merge_requests/new.html.haml
+++ b/app/views/projects/merge_requests/new.html.haml
@@ -1,7 +1,7 @@
- page_title "New Merge Request"
= render "header_title"
-- if @merge_request.can_be_created
+- if @merge_request.can_be_created && !params[:change_branches]
= render 'new_submit'
- else
= render 'new_compare'
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index a71b181a6a5..7f904ec42a0 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -1 +1,5 @@
+.gray-content-block.middle-block.oneline-block
+ = icon("sort-amount-desc")
+ Most recent commits displayed first
+
= render "projects/commits/commits", project: @merge_request.project
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 626970f39be..d9cfc3d7ae9 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,5 +1,5 @@
- if @merge_request_diff.collected?
- = render "projects/diffs/diffs", diffs: @merge_request.diffs, project: @merge_request.project
+ = render "projects/diffs/diffs", diffs: params[:w] == '1' ? @merge_request.diffs_no_whitespace : @merge_request.diffs, project: @merge_request.project
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
- else
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index f18cf96c17d..98f0357ce4e 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -3,11 +3,12 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3 Check out, review and merge locally
+ %h3 Check out, review, and merge locally
.modal-body
%p
- %strong Step 1.
+ %strong Step 1.
Fetch and check out the branch for this merge request
+ = clipboard_button
%pre.dark
- if @merge_request.for_fork?
:preserve
@@ -24,6 +25,7 @@
%p
%strong Step 3.
Merge the branch and fix any conflicts that come up
+ = clipboard_button
%pre.dark
- if @merge_request.for_fork?
:preserve
@@ -36,6 +38,7 @@
%p
%strong Step 4.
Push the result of the merge to GitLab
+ = clipboard_button
%pre.dark
:preserve
git push origin #{h @merge_request.target_branch}
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index 2bf9cd597a4..4dfe46e2b86 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -4,7 +4,7 @@
%span.issue-id Merge Request ##{@merge_request.iid}
%span.creator
&middot;
- created by #{link_to_member(@project, @merge_request.author, size: 24)}
+ opened by #{link_to_member(@project, @merge_request.author, size: 24)}
&middot;
= time_ago_with_tooltip(@merge_request.created_at)
- if @merge_request.updated_at != @merge_request.created_at
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 10640f746f0..ba5ad22bca7 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,4 +1,17 @@
-- if @merge_request.has_ci?
+- ci_commit = @merge_request.ci_commit
+- if ci_commit
+ - status = ci_commit.status
+ .mr-widget-heading
+ .ci_widget{class: "ci-#{status}"}
+ = ci_status_icon(ci_commit)
+ %span CI build #{status}
+ for #{@merge_request.last_commit_short_sha}.
+ %span.ci-coverage
+ = link_to "View build details", ci_status_path(ci_commit)
+
+- elsif @merge_request.has_ci?
+ - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
+ - # Remove in later versions when services like Jenkins will set CI status via Commit status API
.mr-widget-heading
- [:success, :skipped, :canceled, :failed, :running, :pending].each do |status|
.ci_widget{class: "ci-#{status}", style: "display:none"}
@@ -25,6 +38,7 @@
= icon("times-circle")
Could not connect to the CI server. Please check your settings and try again.
- :coffeescript
- $ ->
- merge_request_widget.getCiStatus()
+ :javascript
+ $(function() {
+ merge_request_widget.getCiStatus();
+ });
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index f223f687def..5c6fece8c5c 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -7,25 +7,27 @@
by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)}
#{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
%div
- - if !@merge_request.source_branch_exists?
+ - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true')
= succeed '.' do
The changes were merged into
- %span.label-branch= @merge_request.target_branch
+ = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do
+ = @merge_request.target_branch
The source branch has been removed.
- elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch)
.remove_source_branch_widget
- %p
+ %p
= succeed '.' do
The changes were merged into
- %span.label-branch= @merge_request.target_branch
+ = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do
+ = @merge_request.target_branch
You can remove the source branch now.
= link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do
%i.fa.fa-times
Remove Source Branch
.remove_source_branch_widget.failed.hide
- %p
+ %p
Failed to remove source branch '#{@merge_request.source_branch}'.
.remove_source_branch_in_progress.hide
@@ -33,17 +35,20 @@
= icon('spinner spin')
Removing source branch '#{@merge_request.source_branch}'. Please wait. This page will be automatically reload.
- :coffeescript
- $('.remove_source_branch').on 'click', ->
- $('.remove_source_branch_widget').hide()
- $('.remove_source_branch_in_progress').show()
-
- $(".remove_source_branch").on "ajax:success", (e, data, status, xhr) ->
- location.reload()
-
- $(".remove_source_branch").on "ajax:error", (e, data, status, xhr) ->
- $('.remove_source_branch_widget').hide()
- $('.remove_source_branch_in_progress').hide()
- $('.remove_source_branch_widget.failed').show()
+ :javascript
+ $('.remove_source_branch').on('click', function() {
+ $('.remove_source_branch_widget').hide();
+ $('.remove_source_branch_in_progress').show();
+ });
+
+ $(".remove_source_branch").on("ajax:success", function (e, data, status, xhr) {
+ location.reload();
+ });
+
+ $(".remove_source_branch").on("ajax:error", function (e, data, status, xhr) {
+ $('.remove_source_branch_widget').hide();
+ $('.remove_source_branch_in_progress').hide();
+ $('.remove_source_branch_widget.failed').show();
+ });
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index 613525437ab..9b31014b581 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -1,8 +1,10 @@
+- status_class = @merge_request.ci_commit ? " ci-#{@merge_request.ci_commit.status}" : nil
+
= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f|
= hidden_field_tag :authenticity_token, form_authenticity_token
.accept-merge-holder.clearfix.js-toggle-container
.accept-action
- = f.button class: "btn btn-create accept_merge_request" do
+ = f.button class: "btn btn-create accept_merge_request#{status_class}" do
Accept Merge Request
- if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork?
.accept-control.checkbox
@@ -18,8 +20,9 @@
text: @merge_request.merge_commit_message,
rows: 14, hint: true
- :coffeescript
- $('.accept-mr-form').on 'ajax:before', ->
- btn = $('.accept_merge_request')
- btn.disable()
- btn.html("<i class='fa fa-spinner fa-spin'></i> Merge in progress")
+ :javascript
+ $('.accept-mr-form').on('ajax:before', function() {
+ var btn = $('.accept_merge_request');
+ btn.disable();
+ btn.html("<i class='fa fa-spinner fa-spin'></i> Merge in progress");
+ });
diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml
index b6b8974297e..e16878ba513 100644
--- a/app/views/projects/merge_requests/widget/open/_check.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_check.html.haml
@@ -2,6 +2,8 @@
= icon("spinner spin")
Checking ability to merge automatically&hellip;
-:coffeescript
- $ ->
- merge_request_widget.getMergeStatus()
+:javascript
+ $(function() {
+ merge_request_widget.getMergeStatus();
+ });
+
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 74e9668052d..39aa2437e18 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,10 +1,3 @@
-%h3.page-title= @milestone.new_record? ? "New Milestone" : "Edit Milestone ##{@milestone.iid}"
-.back-link
- = link_to namespace_project_milestones_path(@project.namespace, @project) do
- &larr; To milestones
-
-%hr
-
= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-requires-input'} do |f|
-if @milestone.errors.any?
.alert.alert-danger
@@ -16,16 +9,13 @@
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
- = f.text_field :title, maxlength: 255, class: "form-control", required: true
- %p.hint Required
+ = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
- = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
- .hint
- .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}.
- .pull-left Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
+ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
+ = render 'projects/notes/hints'
.clearfix
.error-alert
.col-md-6
@@ -45,7 +35,7 @@
:javascript
- $( ".datepicker" ).datepicker({
+ $(".datepicker").datepicker({
dateFormat: "yy-mm-dd",
onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
}).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));
diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml
index 88fccfe4981..133d802aaca 100644
--- a/app/views/projects/milestones/_issue.html.haml
+++ b/app/views/projects/milestones/_issue.html.haml
@@ -1,7 +1,7 @@
%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.email, 16), class: "avatar s16", alt: ''
+ = 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}
diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml
index 0d7a118569a..a1033607c5d 100644
--- a/app/views/projects/milestones/_merge_request.html.haml
+++ b/app/views/projects/milestones/_merge_request.html.haml
@@ -5,4 +5,4 @@
= link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title
.pull-right.assignee-icon
- if merge_request.assignee
- = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16", alt: ''
+ = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: ''
diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml
index 5e93d55b1fb..d6a44c9f0a1 100644
--- a/app/views/projects/milestones/_milestone.html.haml
+++ b/app/views/projects/milestones/_milestone.html.haml
@@ -18,11 +18,7 @@
.row
.col-sm-6
- - if milestone.expired? and not milestone.closed?
- %span.cred (Expired)
- - if milestone.expires_at
- %span
- = milestone.expires_at
+ = render 'shared/milestone_expired', milestone: milestone
.col-sm-6
- if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
= link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs edit-milestone-link btn-grouped" do
@@ -31,4 +27,4 @@
= link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close"
= link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
%i.fa.fa-trash-o
- Remove
+ Delete
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index e9dc0b77462..43f8863163d 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,3 +1,9 @@
- page_title "Edit", @milestone.title, "Milestones"
= render "header_title"
+
+%h3.page-title
+ Edit Milestone ##{@milestone.iid}
+
+%hr
+
= render "form"
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index 9ba9acb6f77..0d016f78313 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,3 +1,9 @@
- page_title "New Milestone"
= render "header_title"
+
+%h3.page-title
+ New Milestone
+
+%hr
+
= render "form"
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 4eeb0621e52..c3bda794c65 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,46 +1,50 @@
- page_title @milestone.title, "Milestones"
= render "header_title"
-%h4.page-title
- .issue-box{ class: issue_box_class(@milestone) }
- - if @milestone.closed?
- Closed
- - elsif @milestone.expired?
- Expired
- - else
- Open
- Milestone ##{@milestone.iid}
- %small.creator
- = @milestone.expires_at
- .pull-right
- - if can?(current_user, :admin_milestone, @project)
- = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do
- %i.fa.fa-pencil-square-o
- Edit
- - if @milestone.active?
- = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped"
+.issuable-details
+ .page-title
+ .issue-box{ class: issue_box_class(@milestone) }
+ - if @milestone.closed?
+ Closed
+ - elsif @milestone.expired?
+ Expired
- else
- = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped"
- = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do
- %i.fa.fa-trash-o
- Remove
+ Open
+ Milestone ##{@milestone.iid}
+ - if @milestone.expires_at
+ %span.creator
+ &middot;
+ = @milestone.expires_at
+ .pull-right
+ - if can?(current_user, :admin_milestone, @project)
+ = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do
+ %i.fa.fa-pencil-square-o
+ Edit
+
+ - if @milestone.active?
+ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped"
+ - else
+ = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped"
+
+ = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do
+ %i.fa.fa-trash-o
+ Delete
+
+ .gray-content-block.middle-block
+ %h2.issue-title
+ = gfm escape_once(@milestone.title)
+ %div
+ - if @milestone.description.present?
+ .description
+ .wiki
+ = preserve do
+ = markdown @milestone.description
-%hr
- if @milestone.issues.any? && @milestone.can_be_closed?
- .alert.alert-success
+ .alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close milestone now.
-%h3.issue-title
- = gfm escape_once(@milestone.title)
-%div
- - if @milestone.description.present?
- .description
- .wiki
- = preserve do
- = markdown @milestone.description
-
-%hr
-.context
+.context.prepend-top-default
%p.lead
Progress:
#{@milestone.closed_items_count} closed
@@ -51,8 +55,7 @@
%span.pull-right= @milestone.expires_at
= milestone_progress_bar(@milestone)
-
-%ul.nav.nav-tabs
+%ul.center-top-menu.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
@@ -66,17 +69,21 @@
Participants
%span.badge= @users.count
- .pull-right
- - if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
- %i.fa.fa-plus
- New Issue
- - if can?(current_user, :read_issue, @project)
- = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped"
-
.tab-content
.tab-pane.active#tab-issues
- .row
+ .gray-content-block.middle-block
+ .pull-right
+ - if can?(current_user, :create_issue, @project)
+ = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
+ %i.fa.fa-plus
+ New Issue
+ - if can?(current_user, :read_issue, @project)
+ = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped"
+
+ .oneline
+ All issues in this milestone
+
+ .row.prepend-top-default
.col-md-4
= render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned, id: 'unassigned')
.col-md-4
@@ -85,7 +92,15 @@
= render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed')
.tab-pane#tab-merge-requests
- .row
+ .gray-content-block.middle-block
+ .pull-right
+ - if can?(current_user, :read_merge_request, @project)
+ = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped"
+
+ .oneline
+ All merge requests in this milestone
+
+ .row.prepend-top-default
.col-md-3
= render('merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: @merge_requests.opened.unassigned, id: 'unassigned')
.col-md-3
@@ -100,11 +115,15 @@
= 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.email, 32), class: "avatar s32"
+ = 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/projects/new.html.haml b/app/views/projects/new.html.haml
index bccea21e7a8..25233112132 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -1,5 +1,10 @@
- page_title 'New Project'
-- header_title 'New Project'
+- header_title "Projects", root_path
+
+%h3.page-title
+ New Project
+%hr
+
.project-edit-container
.project-edit-errors
= render 'projects/errors'
@@ -8,23 +13,26 @@
= form_for @project, html: { class: 'new_project form-horizontal js-requires-input' } do |f|
.form-group.project-name-holder
= f.label :path, class: 'control-label' do
- %strong Project path
+ Project path
.col-sm-10
.input-group
- = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 1, autofocus: true, required: true
- .input-group-addon
- \.git
-
- - if current_user.can_select_namespace?
- .form-group
- = f.label :namespace_id, class: 'control-label' do
- %span Namespace
- .col-sm-10
- = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2}
+ - if current_user.can_select_namespace?
+ .input-group-addon
+ = root_url
+ = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2', tabindex: 1}
+ .input-group-addon
+ \/
+ - else
+ .input-group-addon
+ #{root_url}#{current_user.username}/
+ = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true
+
+ - if current_user.can_create_group?
+ .help-block
+ Want to house several dependent projects under the same namespace?
+ = link_to "Create a group", new_group_path
- if import_sources_enabled?
- %hr
-
.project-import.js-toggle-container
.form-group
%label.control-label Import project from
@@ -35,7 +43,7 @@
%i.fa.fa-github
GitHub
- else
- = link_to '#', class: 'how_to_import_link light btn import_github' do
+ = link_to '#', class: 'how_to_import_link btn import_github' do
%i.fa.fa-github
GitHub
= render 'github_import_modal'
@@ -46,7 +54,7 @@
%i.fa.fa-bitbucket
Bitbucket
- else
- = link_to status_import_bitbucket_path, class: 'how_to_import_link light btn import_bitbucket', "data-no-turbolink" => "true" do
+ = link_to status_import_bitbucket_path, class: 'how_to_import_link btn import_bitbucket', "data-no-turbolink" => "true" do
%i.fa.fa-bitbucket
Bitbucket
= render 'bitbucket_import_modal'
@@ -57,7 +65,7 @@
%i.fa.fa-heart
GitLab.com
- else
- = link_to status_import_gitlab_path, class: 'how_to_import_link light btn import_gitlab' do
+ = link_to status_import_gitlab_path, class: 'how_to_import_link btn import_gitlab' do
%i.fa.fa-heart
GitLab.com
= render 'gitlab_import_modal'
@@ -83,39 +91,21 @@
%span Any repo by URL
.js-toggle-content.hide
- .form-group.import-url-data
- = f.label :import_url, class: 'control-label' do
- %span Git repository URL
- .col-sm-10
- = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
- .well.prepend-top-20
- %ul
- %li
- The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
- %li
- The import will time out after 4 minutes. For big repositories, use a clone/push combination.
- %li
- To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}.
-
- %hr.prepend-botton-10
+ = render "shared/import_form", f: f
+
+ .prepend-botton-10
.form-group
= f.label :description, class: 'control-label' do
Description
%span.light (optional)
.col-sm-10
- = f.text_area :description, placeholder: "Awesome project", class: "form-control", rows: 3, maxlength: 250, tabindex: 3
+ = f.text_area :description, class: "form-control", rows: 3, maxlength: 250, tabindex: 3
= render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project
.form-actions
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
-
- - if current_user.can_create_group?
- .pull-right
- .light
- Need a group for several dependent projects?
- = link_to new_group_path, class: "btn btn-xs" do
- Create a group
+ = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
.save-project-loader.hide
.center
@@ -124,9 +114,11 @@
Creating project &amp; repository.
%p Please wait a moment, this page will automatically refresh when ready.
-:coffeescript
- $('.how_to_import_link').bind 'click', (e) ->
- e.preventDefault()
- import_modal = $(this).next(".modal").show()
- $('.modal-header .close').bind 'click', ->
- $(".modal").hide()
+:javascript
+ $('.how_to_import_link').bind('click', function (e) {
+ e.preventDefault();
+ var import_modal = $(this).next(".modal").show();
+ });
+ $('.modal-header .close').bind('click', function() {
+ $(".modal").hide();
+ });
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index a0e26f9827e..3ccda1b381c 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -2,10 +2,9 @@
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f|
= note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field js-quick-submit'
= render 'projects/notes/hints'
.note-form-actions
- .buttons
- = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button'
- = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel'
+ = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button'
+ = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel'
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index d99445da59a..88e711ab534 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,5 +1,5 @@
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f|
- = hidden_field_tag :view, params[:view]
+ = hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= note_target_fields(@note)
= f.hidden_field :commit_id
@@ -8,12 +8,11 @@
= 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'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-quick-submit'
= render 'projects/notes/hints'
.error-alert
- .note-form-actions
- .buttons.clearfix
- = f.submit 'Add Comment', class: "btn comment-btn btn-grouped js-comment-button"
- = yield(:note_actions)
- %a.btn.grouped.js-close-discussion-note-form Cancel
+ .note-form-actions.clearfix
+ = f.submit 'Add Comment', class: "btn btn-create comment-btn btn-grouped js-comment-button"
+ = yield(:note_actions)
+ %a.btn.btn-cancel.js-close-discussion-note-form Cancel
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 9bfbde02ca2..dd0abc8c746 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -1,8 +1,8 @@
%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 } }
.timeline-entry-inner
.timeline-icon
- = link_to user_path(note.author) do
- = image_tag avatar_icon(note.author_email), class: 'avatar s40', alt: ''
+ %a{href: user_path(note.author)}
+ = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
- if note_editable?(note)
@@ -14,10 +14,10 @@
= icon('trash-o')
- unless note.system
- - member = note.project.team.find_member(note.author.id)
- - if member
+ - access = note.project.team.human_max_access(note.author.id)
+ - if access
%span.note-role.label
- = member.human_access
+ = access
= link_to_member(note.project, note.author, avatar: false)
@@ -25,7 +25,7 @@
= '@' + note.author.username
%span.note-last-update
- = link_to "##{dom_id(note)}", name: dom_id(note), title: "Link here" do
+ %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
@@ -35,31 +35,12 @@
- if note.updated_by && note.updated_by != note.author
by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)}
- - if note.superceded?(@notes)
- - if note.upvote?
- %span.vote.upvote.label.label-gray.strikethrough
- = icon('thumbs-up')
- \+1
- - if note.downvote?
- %span.vote.downvote.label.label-gray.strikethrough
- = icon('thumbs-down')
- \-1
- - else
- - if note.upvote?
- %span.vote.upvote.label.label-success
- = icon('thumbs-up')
- \+1
- - if note.downvote?
- %span.vote.downvote.label.label-danger
- = icon('thumbs-down')
- \-1
-
-
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
= preserve do
= markdown(note.note, {no_header_anchors: true})
- = render 'projects/notes/edit_form', note: note
+ - if note_editable?(note)
+ = render 'projects/notes/edit_form', note: note
- if note.attachment.url
.note-attachment
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 04222b8f7c4..99c1b0fa43e 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -4,7 +4,7 @@
.js-main-target-form
- if can? current_user, :create_note, @project
- = render "projects/notes/form", view: params[:view]
+ = render "projects/notes/form", view: diff_view
:javascript
- new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{params[:view]}")
+ new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
index 43e92437cf5..0c73d7e34ac 100644
--- a/app/views/projects/project_members/_group_members.html.haml
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -4,11 +4,11 @@
group members
%small
(#{members.count})
- .panel-head-actions
- = link_to group_group_members_path(@group), class: 'btn btn-sm' do
- %i.fa.fa-pencil-square-o
+ .pull-right
+ = link_to group_group_members_path(@group), class: 'btn' do
+ = icon('pencil-square-o')
Edit group members
- %ul.well-list
+ %ul.content-list
- members.each do |member|
= render 'groups/group_members/group_member', member: member, show_controls: false
- if members.count > 20
diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml
index 860a997cff8..05bf3a7ef6a 100644
--- a/app/views/projects/project_members/_project_member.html.haml
+++ b/app/views/projects/project_members/_project_member.html.haml
@@ -4,7 +4,7 @@
%li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)}
%span.list-item-name
- if member.user
- = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
+ = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
%strong
= link_to user.name, user_path(user)
%span.cgray= user.username
@@ -14,7 +14,7 @@
%label.label.label-danger
%strong Blocked
- else
- = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: ''
+ = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
%strong
= member.invite_email
%span.cgray
@@ -24,18 +24,19 @@
= link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at)
- - if current_user_can_admin_project
+ - if can?(current_user, :admin_project_member, @project)
= link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
Resend invite
- - if current_user_can_admin_project
- - unless @project.personal? && user == current_user
- .pull-right
- %strong= member.human_access
+ - if can?(current_user, :admin_project_member, @project)
+ .pull-right
+ %strong= member.human_access
+ - if can?(current_user, :update_project_member, member)
= button_tag class: "btn-xs btn js-toggle-button",
title: 'Edit access level', type: 'button' do
%i.fa.fa-pencil-square-o
+ - if can?(current_user, :destroy_project_member, member)
&nbsp;
- if current_user == user
= link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index 615c425e59a..ccddab13aaf 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,11 +1,21 @@
-- can_admin_project = can?(current_user, :admin_project, @project)
-
-.panel.panel-default.prepend-top-20
+.panel.panel-default
.panel-heading
%strong #{@project.name}
project members
%small
(#{members.count})
- %ul.well-list
+ .pull-right
+ = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
+ = button_tag class: 'btn', title: 'Search' do
+ = icon("search")
+ %ul.content-list
- members.each do |project_member|
- = render 'project_member', member: project_member, current_user_can_admin_project: can_admin_project
+ = render 'project_member', member: project_member
+
+:javascript
+ $('form.member-search-form').on('submit', function (event) {
+ event.preventDefault();
+ Turbolinks.visit(this.action + '?' + $(this).serialize());
+ });
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 9a0a824b811..29225a36364 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,35 +1,21 @@
- page_title "Members"
= render "header_title"
+- @blank_container = true
-.gray-content-block.top-block
- .clearfix.js-toggle-container
- = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' }
- = button_tag 'Search', class: 'btn'
-
- - if can?(current_user, :admin_project_member, @project)
- %span.pull-right
- = button_tag class: 'btn btn-new btn-grouped js-toggle-button', type: 'button' do
- Add members
- %i.fa.fa-chevron-down
- = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
- Import members
-
- .js-toggle-content.hide.new-group-member-holder
+.project-members-page
+ - if can?(current_user, :admin_project_member, @project)
+ .panel.panel-default
+ .panel-heading
+ Add new user to project
+ .pull-right
+ = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
+ Import members
+ .panel-body
+ %p.light
+ Users with access to this project are listed below.
= render "new_project_member"
-%p.prepend-top-default.light
- Users with access to this project are listed below.
- Read more about project permissions
- %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
-
-= render "team", members: @project_members
-
-- if @group
- = render "group_members", members: @group_members
+ = render "team", members: @project_members
-:coffeescript
- $('form.member-search-form').on 'submit', (event) ->
- event.preventDefault()
- Turbolinks.visit @.action + '?' + $(@).serialize()
+ - if @group
+ = render "group_members", members: @group_members
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 811b1858821..2fb3a41d541 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,3 +1,2 @@
-- can_admin_project = can?(current_user, :admin_project, @project)
:plain
- $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member, current_user_can_admin_project: can_admin_project))}');
+ $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member))}');
diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml
index bb49f4de873..f68449b1863 100644
--- a/app/views/projects/protected_branches/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/_branches_list.html.haml
@@ -1,34 +1,35 @@
- unless @branches.empty?
%br
%h4 Already Protected:
- %table.table.protected-branches-list
- %thead
- %tr.no-border
- %th Branch
- %th Developers can push
- %th Last commit
- %th
+ .table-holder
+ %table.table.protected-branches-list
+ %thead
+ %tr.no-border
+ %th Branch
+ %th Developers can push
+ %th Last commit
+ %th
- %tbody
- - @branches.each do |branch|
- - @url = namespace_project_protected_branch_path(@project.namespace, @project, branch)
- %tr
- %td
- = link_to namespace_project_commits_path(@project.namespace, @project, branch.name) do
- %strong= branch.name
- - if @project.root_ref?(branch.name)
- %span.label.label-info default
+ %tbody
+ - @branches.each do |branch|
+ - @url = namespace_project_protected_branch_path(@project.namespace, @project, branch)
+ %tr
%td
- = check_box_tag "developers_can_push", branch.id, branch.developers_can_push, "data-url" => @url
- %td
- - if commit = branch.commit
- = link_to namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id' do
- = commit.short_id
- &middot;
- #{time_ago_with_tooltip(commit.committed_date)}
- - else
- (branch was removed from repository)
- %td
- .pull-right
- - if can? current_user, :admin_project, @project
- = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm"
+ = link_to namespace_project_commits_path(@project.namespace, @project, branch.name) do
+ %strong= branch.name
+ - if @project.root_ref?(branch.name)
+ %span.label.label-info default
+ %td
+ = check_box_tag "developers_can_push", branch.id, branch.developers_can_push, "data-url" => @url
+ %td
+ - if commit = branch.commit
+ = link_to namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id' do
+ = commit.short_id
+ &middot;
+ #{time_ago_with_tooltip(commit.committed_date)}
+ - else
+ (branch was removed from repository)
+ %td
+ .pull-right
+ - if can? current_user, :admin_project, @project
+ = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm"
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml
index 52b3a50c1e6..2541105b007 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/index.html.haml
@@ -22,7 +22,7 @@
.form-group
= f.label :name, "Branch", class: 'control-label'
.col-sm-10
- = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "select2"})
+ = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: true}, {class: "select2", data: {placeholder: "Select branch"}})
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
@@ -33,4 +33,3 @@
.form-actions
= f.submit 'Protect', class: "btn-create btn"
= render 'branches_list'
-
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
new file mode 100644
index 00000000000..bc80f2f29ad
--- /dev/null
+++ b/app/views/projects/releases/edit.html.haml
@@ -0,0 +1,19 @@
+- page_title "Edit", @tag.name, "Tags"
+= render "projects/commits/header_title"
+= render "projects/commits/head"
+
+.gray-content-block
+ .oneline
+ .title
+ Release notes for tag
+ %strong #{@tag.name}
+
+.prepend-top-default
+ = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form' }) do |f|
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
+ = render 'projects/zen', f: f, attr: :description, classes: 'description js-quick-submit form-control'
+ = render 'projects/notes/hints'
+ .error-alert
+ .form-actions.prepend-top-default
+ = f.submit 'Save changes', class: 'btn btn-save'
+ = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel"
diff --git a/app/views/projects/remove_fork.js.haml b/app/views/projects/remove_fork.js.haml
new file mode 100644
index 00000000000..17b9fecfeb1
--- /dev/null
+++ b/app/views/projects/remove_fork.js.haml
@@ -0,0 +1,2 @@
+:plain
+ location.href = "#{edit_namespace_project_path(@project.namespace, @project)}";
diff --git a/app/views/ci/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index ef8622e2807..e6b8a2e6fe7 100644
--- a/app/views/ci/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -3,9 +3,9 @@
= runner_status_icon(runner)
%span.monospace
- if @runners.include?(runner)
- = link_to runner.short_sha, ci_project_runner_path(@project, runner)
+ = link_to runner.short_sha, runner_path(runner)
%small
- =link_to edit_ci_project_runner_path(@project, runner) do
+ =link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do
%i.fa.fa-edit.btn
- else
= runner.short_sha
@@ -13,12 +13,12 @@
.pull-right
- if @runners.include?(runner)
- if runner.belongs_to_one_project?
- = link_to 'Remove runner', [:ci, @project, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ = link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- else
- - runner_project = @project.runner_projects.find_by(runner_id: runner)
- = link_to 'Disable for this project', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ - runner_project = @ci_project.runner_projects.find_by(runner_id: runner)
+ = link_to 'Disable for this project', [:ci, @ci_project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- elsif runner.specific?
- = form_for [:ci, @project, @project.runner_projects.new] do |f|
+ = form_for [:ci, @ci_project, @ci_project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: runner.id
= f.submit 'Enable for this project', class: 'btn btn-sm'
.pull-right
@@ -32,4 +32,3 @@
- runner.tag_list.each do |tag|
%span.label.label-primary
= tag
-
diff --git a/app/views/ci/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 944b3fd930d..316ea747b14 100644
--- a/app/views/ci/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -3,11 +3,11 @@
.bs-callout.bs-callout-warning
GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X.
%hr
- - if @project.shared_runners_enabled
- = link_to toggle_shared_runners_ci_project_path(@project), class: 'btn btn-warning', method: :post do
+ - if @ci_project.shared_runners_enabled
+ = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-warning', method: :post do
Disable shared runners
- else
- = link_to toggle_shared_runners_ci_project_path(@project), class: 'btn btn-success', method: :post do
+ = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-success', method: :post do
Enable shared runners
&nbsp; for this project
@@ -17,7 +17,7 @@
- else
%h4.underlined-title Available shared runners - #{@shared_runners_count}
%ul.bordered-list.available-shared-runners
- = render @shared_runners.first(10)
+ = render partial: 'runner', collection: @shared_runners, as: :runner
- if @shared_runners_count > 10
.light
and #{@shared_runners_count - 10} more...
diff --git a/app/views/ci/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index 0604e7a46c5..c13625c7e49 100644
--- a/app/views/ci/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -12,7 +12,7 @@
%code #{ci_root_url(only_path: false)}
%li
Use the following registration token during setup:
- %code #{@project.token}
+ %code #{@ci_project.token}
%li
Start runner!
@@ -20,10 +20,10 @@
- if @runners.any?
%h4.underlined-title Runners activated for this project
%ul.bordered-list.activated-specific-runners
- = render @runners
+ = render partial: 'runner', collection: @runners, as: :runner
- if @specific_runners.any?
%h4.underlined-title Available specific runners
%ul.bordered-list.available-specific-runners
- = render @specific_runners
+ = render partial: 'runner', collection: @specific_runners, as: :runner
= paginate @specific_runners
diff --git a/app/views/ci/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml
index 81c8e58ae2b..eba03028af8 100644
--- a/app/views/ci/runners/edit.html.haml
+++ b/app/views/projects/runners/edit.html.haml
@@ -1,6 +1,8 @@
+- page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners"
+
%h4 Runner ##{@runner.id}
%hr
-= form_for [:ci, @project, @runner], html: { class: 'form-horizontal' } do |f|
+= form_for @runner, url: runner_path(@runner), html: { class: 'form-horizontal' } do |f|
.form-group
= label :active, "Active", class: 'control-label'
.col-sm-10
@@ -21,7 +23,7 @@
= label_tag :tag_list, class: 'control-label' do
Tags
.col-sm-10
- = f.text_field :tag_list, class: 'form-control'
+ = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control'
.help-block You can setup jobs to only use runners with specific tags
.form-actions
- = f.submit 'Save', class: 'btn btn-save'
+ = f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/ci/runners/index.html.haml b/app/views/projects/runners/index.html.haml
index 529fb9c296d..315afe4a764 100644
--- a/app/views/ci/runners/index.html.haml
+++ b/app/views/projects/runners/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Runners"
.light
%p
A 'runner' is a process which runs a build.
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
new file mode 100644
index 00000000000..5bf4c09ca25
--- /dev/null
+++ b/app/views/projects/runners/show.html.haml
@@ -0,0 +1,66 @@
+- page_title "#{@runner.description} ##{@runner.id}", "Runners"
+
+%h3.page-title
+ Runner ##{@runner.id}
+ .pull-right
+ - if @runner.shared?
+ %span.runner-state.runner-state-shared
+ Shared
+ - else
+ %span.runner-state.runner-state-specific
+ Specific
+
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %th Property Name
+ %th Value
+ %tr
+ %td
+ Tags
+ %td
+ - @runner.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+ %tr
+ %td
+ Name
+ %td
+ = @runner.name
+ %tr
+ %td
+ Version
+ %td
+ = @runner.version
+ %tr
+ %td
+ Revision
+ %td
+ = @runner.revision
+ %tr
+ %td
+ Platform
+ %td
+ = @runner.platform
+ %tr
+ %td
+ Architecture
+ %td
+ = @runner.architecture
+ %tr
+ %td
+ Description
+ %td
+ = @runner.description
+ %tr
+ %td
+ Last contact
+ %td
+ - if @runner.contacted_at
+ #{time_ago_in_words(@runner.contacted_at)} ago
+ - else
+ Never
+
+
+
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index e1823b51198..1b70880043a 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -4,18 +4,15 @@
%p= @service.description
-.back-link
- = link_to namespace_project_services_path(@project.namespace, @project) do
- &larr; to services
-
%hr
= form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form|
= render 'shared/service_settings', form: form
.form-actions
- = form.submit 'Save', class: 'btn btn-save'
+ = form.submit 'Save changes', class: 'btn btn-save'
&nbsp;
- if @service.valid? && @service.activated?
- disabled = @service.can_test? ? '':'disabled'
= link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: "btn #{disabled}"
+ = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml
index 1065def693b..c1356f6db02 100644
--- a/app/views/projects/services/index.html.haml
+++ b/app/views/projects/services/index.html.haml
@@ -2,22 +2,23 @@
%h3.page-title Project services
%p.light Project services allow you to integrate GitLab with other applications
-%table.table
- %thead
- %tr
- %th
- %th Service
- %th Description
- %th Last edit
- - @services.sort_by(&:title).each do |service|
- %tr
- %td
- = boolean_to_icon service.activated?
- %td
- = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do
- %strong= service.title
- %td
- = service.description
- %td.light
- = time_ago_in_words service.updated_at
- ago
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %th
+ %th Service
+ %th Description
+ %th Last edit
+ - @services.sort_by(&:title).each do |service|
+ %tr
+ %td
+ = boolean_to_icon service.activated?
+ %td
+ = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do
+ %strong= service.title
+ %td
+ = service.description
+ %td.light
+ = time_ago_in_words service.updated_at
+ ago
diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder
index 242684e5c7c..15c49767556 100644
--- a/app/views/projects/show.atom.builder
+++ b/app/views/projects/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_url(@project.namespace, @project)
- xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 6a5fc689803..9c7a5584da9 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -2,16 +2,16 @@
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity")
-- if current_user && can?(current_user, :download_code, @project)
- = render 'shared/no_ssh'
- = render 'shared/no_password'
+= content_for :flash_message do
+ - if current_user && can?(current_user, :download_code, @project)
+ = render 'shared/no_ssh'
+ = render 'shared/no_password'
-- if prefer_readme?
- = render 'projects/last_push'
+= render 'projects/last_push'
= render "home_panel"
-.project-stats.gray-content-block
+.project-stats.gray-content-block.second-block
%ul.nav.nav-pills
%li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
@@ -27,7 +27,7 @@
= link_to project_files_path(@project) do
= repository_size
- - if !prefer_readme? && @repository.readme
+ - if default_project_view != 'readme' && @repository.readme
%li
= link_to 'Readme', readme_path(@project)
@@ -63,19 +63,17 @@
= icon("exclamation-triangle fw")
Archived project! Repository is read-only
-%section
- - if prefer_readme?
- .project-show-readme
- = render 'projects/readme'
- - else
- .project-show-activity
- = render 'projects/activity'
+- if @repository.commit
+ .content-block.second-block.white
+ = render 'projects/last_commit', commit: @repository.commit, project: @project
+%div{class: "project-show-#{default_project_view}"}
+ = render default_project_view
- if current_user
- access = user_max_access_in_project(current_user, @project)
- if access
- .prepend-top-20
+ .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)
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
new file mode 100644
index 00000000000..4a515469422
--- /dev/null
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -0,0 +1,11 @@
+= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do
+ = icon('plus')
+ New Snippet
+- if can?(current_user, :admin_project_snippet, @snippet)
+ = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do
+ = icon('trash-o')
+ Delete
+- if can?(current_user, :update_project_snippet, @snippet)
+ = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
+ = icon('pencil-square-o')
+ Edit
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index e69f2d99709..dc3ea1fcf12 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -2,6 +2,6 @@
= render "header_title"
%h3.page-title
- Edit snippet
+ Edit Snippet
%hr
= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 3fed2c9949d..4af963e14da 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,17 +1,13 @@
- page_title "Snippets"
= render "header_title"
-%h3.page-title
- Snippets
- - if can? current_user, :create_project_snippet, @project
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Snippet" do
- Add new snippet
+.gray-content-block.top-block
+ .pull-right
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do
+ = icon('plus')
+ New Snippet
-%p.light
- Share code pastes with others out of git repository
+ .oneline
+ Share code pastes with others out of git repository
-%ul.bordered-list
- = render partial: "shared/snippets/snippet", collection: @snippets
- - if @snippets.empty?
- %li
- .nothing-here-block Nothing here.
+= render 'snippets/snippets'
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index 67cd69fd215..e57237991b4 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -2,6 +2,6 @@
= render "header_title"
%h3.page-title
- New snippet
+ New Snippet
%hr
= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index be7d4d486fa..5d706942f2d 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,40 +1,18 @@
- page_title @snippet.title, "Snippets"
= render "header_title"
-%h3.page-title
- = @snippet.title
+.snippet-holder
+ = render 'shared/snippets/header'
- .pull-right
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do
- Add new snippet
+ %article.file-holder
+ .file-title
+ = blob_icon 0, @snippet.file_name
+ %strong
+ = @snippet.file_name
+ .file-actions.hidden-xs
+ .btn-group.tree-btn-group
+ = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
-%hr
+ = render 'shared/snippets/blob'
-.append-bottom-20
- .pull-right
- = "##{@snippet.id}"
- %span.light
- by
- = link_to user_path(@snippet.author) do
- = image_tag avatar_icon(@snippet.author_email), class: "avatar avatar-inline s16"
- = @snippet.author_name
-
- .back-link
- = link_to namespace_project_snippets_path(@project.namespace, @project) do
- &larr; project snippets
-
-.file-holder
- .file-title
- %i.fa.fa-file
- %strong
- = @snippet.file_name
- .file-actions
- .btn-group
- - if can?(current_user, :update_project_snippet, @snippet)
- = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", title: 'Edit Snippet'
- = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
- - if can?(current_user, :admin_project_snippet, @snippet)
- = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet'
- = render 'shared/snippets/blob'
-
-%div#notes= render "projects/notes/notes_with_form"
+ %div#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml
new file mode 100644
index 00000000000..667057ef2d8
--- /dev/null
+++ b/app/views/projects/tags/_download.html.haml
@@ -0,0 +1,17 @@
+%span.btn-group.btn-grouped
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do
+ %i.fa.fa-download
+ %span source code
+ %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' }
+ %span.caret
+ %span.sr-only
+ Select Archive Format
+ %ul.col-xs-10.dropdown-menu{ role: 'menu' }
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download zip
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar.gz
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 2ca295fc5f3..e2c5178185e 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -1,22 +1,28 @@
- commit = @repository.commit(tag.target)
+- release = @releases.find { |release| release.tag == tag.name }
%li
%div
- = link_to namespace_project_commits_path(@project.namespace, @project, tag.name), class: "" do
+ = link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
%strong
- %i.fa.fa-tag
+ = icon('tag')
= tag.name
- if tag.message.present?
&nbsp;
= strip_gpg_signature(tag.message)
+
.controls
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn' do
+ = icon("pencil")
- if can? current_user, :download_code, @project
- = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-xs'
- - if can?(current_user, :admin_project, @project)
- = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-xs btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do
- %i.fa.fa-trash-o
+ = render 'projects/tags/download', ref: tag.name, project: @project
- if commit
= render 'projects/branches/commit', commit: commit, project: @project
- else
%p
Cant find HEAD commit for this tag
+ - if release && release.description.present?
+ .description.prepend-top-default
+ .wiki
+ = preserve do
+ = markdown release.description
diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml
deleted file mode 100644
index ada6710f940..00000000000
--- a/app/views/projects/tags/destroy.js.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-$('.js-totaltags-count').html("#{@repository.tags.size}")
-- if @repository.tags.size == 0
- $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 85d76eae3b5..760347de0a9 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -6,7 +6,7 @@
- if can? current_user, :push_code, @project
.pull-right
= link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
- %i.fa.fa-add-sign
+ = icon('plus')
New tag
.oneline
Tags give the ability to mark specific points in history as being important
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 9f5c1be125c..3a2f75fecaa 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -5,33 +5,42 @@
.alert.alert-danger
%button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
= @error
+
%h3.page-title
- %i.fa.fa-code-fork
- New tag
-= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal" do
+ New Tag
+%hr
+
+= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-requires-input" do
.form-group
- = label_tag :tag_name, 'Name for new tag', class: 'control-label'
+ = label_tag :tag_name, nil, class: 'control-label'
.col-sm-10
- = text_field_tag :tag_name, params[:tag_name], placeholder: 'v3.0.1', required: true, tabindex: 1, class: 'form-control'
+ = text_field_tag :tag_name, params[:tag_name], required: true, tabindex: 1, autofocus: true, class: 'form-control'
.form-group
= label_tag :ref, 'Create from', class: 'control-label'
.col-sm-10
- = text_field_tag :ref, params[:ref], placeholder: 'master', required: true, tabindex: 2, class: 'form-control'
- .light Branch name or commit SHA
+ = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control'
+ .help-block Branch name or commit SHA
+ .form-group
+ = label_tag :message, nil, class: 'control-label'
+ .col-sm-10
+ = text_field_tag :message, nil, required: false, tabindex: 3, class: 'form-control'
+ .help-block Optionally, enter a message to create an annotated tag.
+ %hr
.form-group
- = label_tag :message, 'Message', class: 'control-label'
+ = label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10
- = text_field_tag :message, nil, placeholder: 'Enter message.', required: false, tabindex: 3, class: 'form-control'
- .light (Optional) Entering a message will create an annotated tag.
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
+ = render 'projects/zen', attr: :release_description, classes: 'description js-quick-submit form-control'
+ = render 'projects/notes/hints'
+ .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
.form-actions
= button_tag 'Create tag', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel'
:javascript
- disableButtonIfAnyEmptyField($("#new-tag-form"), ".form-control", ".btn-create");
- var availableTags = #{@project.repository.ref_names.to_json};
+ var availableRefs = #{@project.repository.ref_names.to_json};
$("#ref").autocomplete({
- source: availableTags,
+ source: availableRefs,
minLength: 1
});
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
new file mode 100644
index 00000000000..879c6c7d310
--- /dev/null
+++ b/app/views/projects/tags/show.html.haml
@@ -0,0 +1,39 @@
+- page_title @tag.name, "Tags"
+= render "projects/commits/header_title"
+= render "projects/commits/head"
+
+.gray-content-block
+ .pull-right
+ - if can?(current_user, :push_code, @project)
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn', title: 'Edit release notes' do
+ = icon("pencil")
+ = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped', title: 'Browse source code' do
+ = icon('files-o')
+ = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped', title: 'Browse commits' do
+ = icon('history')
+ - if can? current_user, :download_code, @project
+ = render 'projects/tags/download', ref: @tag.name, project: @project
+ - if can?(current_user, :admin_project, @project)
+ .pull-right
+ = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped', method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
+ %i.fa.fa-trash-o
+ .title
+ %strong= @tag.name
+ - if @tag.message.present?
+ %span.light
+ &nbsp;
+ = strip_gpg_signature(@tag.message)
+ - if @commit
+ = render 'projects/branches/commit', commit: @commit, project: @project
+ - else
+ Cant find HEAD commit for this tag
+
+
+.append-bottom-default.prepend-top-default
+ - if @release.description.present?
+ .description
+ .wiki
+ = preserve do
+ = markdown @release.description
+ - else
+ This tag has no release notes.
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index 02ecbade219..2ddc5d504fa 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -4,5 +4,5 @@
%span.str-truncated
= link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name))
%td.tree_time_ago.cgray
- = render 'spinner'
+ = render 'projects/tree/spinner'
%td.hidden-xs.tree_commit
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index f082d711865..3c5edf4b033 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,7 +1,8 @@
-%article.readme-holder#README
- = link_to '#README' do
- %h4.readme-file-title
- %i.fa.fa-file
- = readme.name
- .wiki
+%article.file-holder.readme-holder
+ .file-title
+ = blob_icon readme.mode, readme.name
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do
+ %strong
+ = readme.name
+ .file-content.wiki
= render_readme(readme)
diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml
deleted file mode 100644
index 367a87927d7..00000000000
--- a/app/views/projects/tree/_tree.html.haml
+++ /dev/null
@@ -1,53 +0,0 @@
-%ul.breadcrumb.repo-breadcrumb
- %li
- = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
- = @project.path
- - tree_breadcrumbs(tree, 6) do |title, path|
- %li
- - if path
- = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- - else
- = link_to title, '#'
- - if current_user && can_push_branch?(@project, @ref)
- %li
- = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'New file', id: 'new-file-link' do
- %small
- %i.fa.fa-plus
-
-%div#tree-content-holder.tree-content-holder.prepend-top-20
- %table#tree-slider{class: "table_#{@hex_path} tree-table" }
- %thead
- %tr
- %th Name
- %th Last Update
- %th.hidden-xs
- .pull-left Last Commit
- .last-commit.hidden-sm.pull-left
- &nbsp;
- %i.fa.fa-angle-right
- &nbsp;
- %small.light
- = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit)
- &ndash;
- = truncate(@commit.title, length: 50)
- = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right'
-
- - if @path.present?
- %tr.tree-item
- %td.tree-item-file-name
- = link_to "..", namespace_project_tree_path(@project.namespace, @project, up_dir_path), class: 'prepend-left-10'
- %td
- %td.hidden-xs
-
- = render_tree(tree)
-
- - if tree.readme
- = render "projects/tree/readme", readme: tree.readme
-
-%div.tree_progress
-
-:javascript
- // Load last commit log for each file in tree
- $('#tree-slider').waitForImages(function() {
- ajaxGet("#{escape_javascript(@logs_path)}");
- });
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
new file mode 100644
index 00000000000..1bc90edd8f0
--- /dev/null
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -0,0 +1,40 @@
+%div.tree-content-holder
+ .table-holder
+ %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" }
+ %thead
+ %tr
+ %th Name
+ %th Last Update
+ %th.hidden-xs
+ .pull-left Last Commit
+ .last-commit.hidden-sm.pull-left
+ &nbsp;
+ %i.fa.fa-angle-right
+ &nbsp;
+ %small.light
+ = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
+ &ndash;
+ = truncate(@commit.title, length: 50)
+ = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right'
+
+ - if @path.present?
+ %tr.tree-item
+ %td.tree-item-file-name
+ = link_to "..", namespace_project_tree_path(@project.namespace, @project, up_dir_path), class: 'prepend-left-10'
+ %td
+ %td.hidden-xs
+
+ = render_tree(tree)
+
+ - if tree.readme
+ = render "projects/tree/readme", readme: tree.readme
+
+- if allowed_tree_edit?
+ = render 'projects/blob/upload', title: 'Upload New File', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post
+ = render 'projects/blob/new_dir'
+
+:javascript
+ // Load last commit log for each file in tree
+ $('#tree-slider').waitForImages(function() {
+ ajaxGet("#{escape_javascript(@logs_path)}");
+ });
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
new file mode 100644
index 00000000000..1115ca6b4ca
--- /dev/null
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -0,0 +1,32 @@
+.tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'tree', path: @path
+
+%ul.breadcrumb.repo-breadcrumb
+ %li
+ = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
+ = @project.path
+ - tree_breadcrumbs(tree, 6) do |title, path|
+ %li
+ - if path
+ = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
+ - else
+ = link_to title, '#'
+ - if allowed_tree_edit?
+ %li
+ %span.dropdown
+ %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"}
+ = icon('plus')
+ %ul.dropdown-menu
+ %li
+ = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'Create file', id: 'new-file-link' do
+ = icon('pencil fw')
+ Create file
+ %li
+ = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do
+ = icon('file fw')
+ Upload file
+ %li.divider
+ %li
+ = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do
+ = icon('folder fw')
+ New directory
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index e87138bf980..cf65057e704 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -5,5 +5,5 @@
- path = flatten_tree(tree_item)
= link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path))
%td.tree_time_ago.cgray
- = render 'spinner'
+ = render 'projects/tree/spinner'
%td.hidden-xs.tree_commit
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index dec4677f830..ec14bd7f65a 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -6,12 +6,12 @@
= render 'projects/last_push'
-.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'tree', path: @path
-
- if can? current_user, :download_code, @project
.tree-download-holder
= render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true
#tree-holder.tree-holder.clearfix
- = render "tree", tree: @tree
+ .gray-content-block.top-block
+ = render 'projects/tree/tree_header', tree: @tree
+
+ = render 'projects/tree/tree_content', tree: @tree
diff --git a/app/views/ci/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index addfbfcb0d4..48b3b5c9920 100644
--- a/app/views/ci/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -11,4 +11,4 @@
%td
.pull-right
- = link_to 'Revoke', ci_project_trigger_path(@project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-danger btn-sm btn-grouped"
+ = link_to 'Revoke', namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-danger btn-sm btn-grouped"
diff --git a/app/views/ci/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
index 44374a1a4d5..b3ad79a200e 100644
--- a/app/views/ci/triggers/index.html.haml
+++ b/app/views/projects/triggers/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Triggers"
%h3.page-title
Triggers
@@ -7,16 +8,17 @@
%hr.clearfix
-if @triggers.any?
- %table.table
- %thead
- %th Token
- %th Last used
- %th
- = render @triggers
+ .table-holder
+ %table.table
+ %thead
+ %th Token
+ %th Last used
+ %th
+ = render partial: 'trigger', collection: @triggers, as: :trigger
- else
%h4 No triggers
-= form_for [:ci, @project, @trigger], html: { class: 'form-horizontal' } do |f|
+= form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create'), html: { class: 'form-horizontal' } do |f|
.clearfix
= f.submit "Add Trigger", class: 'btn btn-success pull-right'
@@ -34,7 +36,7 @@
:plain
curl -X POST \
-F token=TOKEN \
- #{ci_build_trigger_url(@project.id, 'REF_NAME')}
+ #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')}
%h3
Use .gitlab-ci.yml
@@ -49,7 +51,7 @@
trigger:
type: deploy
script:
- - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@project.id, 'REF_NAME')}"
+ - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')}"
%h3
Pass build variables
@@ -64,4 +66,4 @@
curl -X POST \
-F token=TOKEN \
-F "variables[RUN_NIGHTLY_BUILD]=true" \
- #{ci_build_trigger_url(@project.id, 'REF_NAME')}
+ #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')}
diff --git a/app/views/ci/variables/show.html.haml b/app/views/projects/variables/show.html.haml
index ebf68341e08..e052da1ac43 100644
--- a/app/views/ci/variables/show.html.haml
+++ b/app/views/projects/variables/show.html.haml
@@ -1,21 +1,22 @@
+- page_title "Variables"
%h3.page-title
Secret Variables
-%p.light
+%p.light
These variables will be set to environment by the runner and will be hidden in the build log.
%br
- So you can use them for passwords, secret keys or whatever you want.
+ So you can use them for passwords, secret keys or whatever you want.
%hr
-= nested_form_for @project, url: url_for(controller: 'ci/variables', action: 'update'), html: { class: 'form-horizontal' } do |f|
+= nested_form_for @ci_project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f|
- if @project.errors.any?
#error_explanation
- %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:"
+ %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:"
.alert.alert-error
%ul
- - @project.errors.full_messages.each do |msg|
+ - @ci_project.errors.full_messages.each do |msg|
%li= msg
= f.fields_for :variables do |variable_form|
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 05d754adbe5..1d257818dcd 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default' } do |f|
-if @page.errors.any?
#error_explanation
.alert.alert-danger
@@ -11,24 +11,20 @@
.col-sm-10
= f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: "form-control"
- .row
- .col-sm-offset-2.col-sm-10
- %p.cgray
- To link to a (new) page you can just type
- %code [Link Title](page-slug)
- \.
-
- .form-group.wiki-content
+ .form-group
= f.label :content, class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
- = render 'projects/zen', f: f, attr: :content, classes: 'description form-control'
- .col-sm-12.hint
- .pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}
- .pull-right Attach files by dragging &amp; dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
+ = render 'projects/zen', f: f, attr: :content, classes: 'description form-control js-quick-submit'
+ = render 'projects/notes/hints'
.clearfix
.error-alert
+
+ .help-block
+ To link to a (new) page, simply type
+ %code [Link Title](page-slug)
+ \.
.form-group
= f.label :commit_message, class: 'control-label'
.col-sm-10= f.text_field :message, class: 'form-control', rows: 18
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 14f25822259..29bf5d62abe 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,9 +1,4 @@
%span.pull-right
- - if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new btn-grouped", "data-toggle" => "modal" do
- %i.fa.fa-plus
- New Page
-
- if (@page && @page.persisted?)
= link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
Page History
@@ -11,5 +6,7 @@
= link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
%i.fa.fa-pencil-square-o
Edit
-
-= render 'projects/wikis/new'
+ - if can?(current_user, :admin_wiki, @project)
+ = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
+ = icon('trash')
+ Delete
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index fffb4eb31ab..e6e6ad5bc4b 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,10 +1,19 @@
-%ul.center-top-menu
- = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
- = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
+.project-issuable-filter
+ .controls
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ %i.fa.fa-plus
+ New Page
- = nav_link(path: 'wikis#pages') do
- = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
+ = render 'projects/wikis/new'
- = nav_link(path: 'wikis#git_access') do
- = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
- Git Access
+ %ul.center-top-menu
+ = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
+ = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
+
+ = nav_link(path: 'wikis#pages') do
+ = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
+
+ = nav_link(path: 'wikis#git_access') do
+ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
+ Git Access
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index dace172438c..f0547e9c057 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -12,5 +12,5 @@
The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and /
%p.hint
Please don't use spaces.
- .modal-footer
- = link_to 'Build', '#', class: 'build-new-wiki btn btn-create'
+ .form-actions
+ = link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 0b709c3695b..23f64fbbd10 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,16 +1,16 @@
-- page_title "Edit", @page.title, "Wiki"
+- page_title "Edit", @page.title.capitalize, "Wiki"
= render "header_title"
= render 'nav'
-.pull-right
- = render 'main_links'
-%h3.page-title
- Editing -
- %span.light #{@page.title}
-%hr
-= render 'form'
+.gray-content-block
+ .pull-right
+ = render 'main_links'
+
+ %h3.page-title.oneline
+ %span.light Edit Page
+ - if @page.persisted?
+ = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
+ - else
+ = @page.title
-.pull-right
- - if @page.persisted? && can?(current_user, :admin_wiki, @project)
- = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-sm btn-remove" do
- Delete this page
+= render 'form'
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 6417ef4a38b..11c8c4f0eba 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -5,7 +5,7 @@
.gray-content-block
.row
.col-sm-6
- %h3.page-title
+ %h3.page-title.oneline
Git access for
%strong= @project_wiki.path_with_namespace
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index bfbef823b35..4322146ce34 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -7,28 +7,29 @@
%span.light History for
= link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
-%table.table
- %thead
- %tr
- %th Page version
- %th Author
- %th Commit Message
- %th Last updated
- %th Format
- %tbody
- - @page.versions.each_with_index do |version, index|
- - commit = version
+.table-holder
+ %table.table
+ %thead
%tr
- %td
- = link_to project_wiki_path_with_version(@project, @page,
- commit.id, index == 0) do
- = truncate_sha(commit.id)
- %td
- = commit.author.name
- %td
- = commit.message
- %td
- #{time_ago_with_tooltip(version.authored_date)}
- %td
- %strong
- = @page.page.wiki.page(@page.page.name, commit.id).try(:format)
+ %th Page version
+ %th Author
+ %th Commit Message
+ %th Last updated
+ %th Format
+ %tbody
+ - @page.versions.each_with_index do |version, index|
+ - commit = version
+ %tr
+ %td
+ = link_to project_wiki_path_with_version(@project, @page,
+ commit.id, index == 0) do
+ = truncate_sha(commit.id)
+ %td
+ = commit.author.name
+ %td
+ = commit.message
+ %td
+ #{time_ago_with_tooltip(version.authored_date)}
+ %td
+ %strong
+ = @page.page.wiki.page(@page.page.name, commit.id).try(:format)
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index 03e6a522b25..aae1ad69ad9 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,10 +1,10 @@
-- page_title "All Pages", "Wiki"
+- page_title "Pages", "Wiki"
= render "header_title"
= render 'nav'
.gray-content-block
- %h3.page-title
- All Pages
+ All pages in this wiki are listed below.
+
%ul.content-list
- @wiki_pages.each do |wiki_page|
%li
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 55fbf5a8b6e..309d40f52bc 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -5,11 +5,12 @@
.gray-content-block
= render 'main_links'
- %h3.page-title
+ %h3.page-title.oneline
= @page.title.capitalize
- .wiki-last-edit-by
- Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+ %span.wiki-last-edit-by
+ &middot;
+ last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
- if @page.historical?
.warning_message
@@ -21,8 +22,3 @@
.wiki
= preserve do
= render_wiki_content(@page)
-
-.gray-content-block.footer-block
- .wiki-last-edit-by
- Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
-
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index d637abfa76b..481451edb23 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -42,6 +42,13 @@
Wiki
%span.badge
= @search_results.wiki_blobs_count
+ %li{class: ("active" if @scope == 'commits')}
+ = link_to search_filter_path(scope: 'commits') do
+ = icon('history fw')
+ %span
+ Commits
+ %span.badge
+ = @search_results.commits_count
- elsif @show_snippets
%li{class: ("active" if @scope == 'snippet_blobs')}
diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml
index 3938c545cad..17b0981f073 100644
--- a/app/views/search/_form.html.haml
+++ b/app/views/search/_form.html.haml
@@ -6,7 +6,7 @@
.search-holder.clearfix
.input-group
- = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input", id: "dashboard_search", autofocus: true
+ = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input", id: "dashboard_search", autofocus: true, spellcheck: false
%span.input-group-btn
= button_tag 'Search', class: "btn btn-primary"
- unless params[:snippets].eql? 'true'
diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml
new file mode 100644
index 00000000000..4e6c3965dc6
--- /dev/null
+++ b/app/views/search/results/_commit.html.haml
@@ -0,0 +1,2 @@
+.search-result-row
+ = render 'projects/commits/commit', project: @project, commit: commit
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 2cd422e772a..edb5778f424 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -2,26 +2,9 @@
.git-clone-holder.input-group
.input-group-addon.git-protocols
.input-group-btn
- %button{ |
- type: 'button', |
- class: "btn #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", |
- :"data-clone" => project.ssh_url_to_repo, |
- :"data-title" => "Add an SSH key to your profile<br> to pull or push via SSH",
- :"data-html" => "true",
- :"data-container" => "body"}
- SSH
+ = ssh_clone_button(project)
.input-group-btn
- %button{ |
- type: 'button', |
- class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", |
- :"data-clone" => project.http_url_to_repo, |
- :"data-title" => "Set a password on your account<br> to pull or push via #{gitlab_config.protocol.upcase}",
- :"data-html" => "true",
- :"data-container" => "body"}
- = gitlab_config.protocol.upcase
+ = http_clone_button(project)
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
- - if project.kind_of?(Project)
- .input-group-addon
- .visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" }
- = visibility_level_icon(project.visibility_level)
- = visibility_level_label(project.visibility_level).downcase
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#project_clone')
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 5071ff640f1..7c57924277e 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -1,13 +1,15 @@
.form-group.commit_message-group
- = label_tag 'commit_message', class: 'control-label' do
+ - nonce = SecureRandom.hex
+ = label_tag "commit_message-#{nonce}", class: 'control-label' do
Commit message
.col-sm-10
.commit-message-container
.max-width-marker
= text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text]),
- class: 'form-control', placeholder: local_assigns[:placeholder],
- required: true, rows: (local_assigns[:rows] || 3)
+ class: 'form-control js-commit-message js-quick-submit', placeholder: local_assigns[:placeholder],
+ required: true, rows: (local_assigns[:rows] || 3),
+ id: "commit_message-#{nonce}"
- if local_assigns[:hint]
%p.hint
Try to keep the first line under 52 characters
diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml
index 5f51b0d450f..34241cd8aad 100644
--- a/app/views/shared/_confirm_modal.html.haml
+++ b/app/views/shared/_confirm_modal.html.haml
@@ -3,7 +3,8 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h4 Confirmation required
+ %h3.page-title
+ Confirmation required
.modal-body
%p.cred.lead.js-confirm-text
@@ -14,9 +15,9 @@
%br
Please type
%code.js-confirm-danger-match #{phrase}
- to proceed or close this modal to cancel
+ to proceed or close this modal to cancel.
.form-group
= text_field_tag 'confirm_name_input', '', class: 'form-control js-confirm-danger-input'
- .form-group
+ .form-actions
= submit_tag 'Confirm', class: "btn btn-danger js-confirm-danger-submit"
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
index 45ec49280d2..8d6e16f74c3 100644
--- a/app/views/shared/_field.html.haml
+++ b/app/views/shared/_field.html.haml
@@ -8,7 +8,10 @@
- help = field[:help]
.form-group
- = form.label name, title, class: "control-label"
+ - if type == "password" && value.present?
+ = form.label name, "Change #{title}", class: "control-label"
+ - else
+ = form.label name, title, class: "control-label"
.col-sm-10
- if type == 'text'
= form.text_field name, class: "form-control", placeholder: placeholder
@@ -19,6 +22,6 @@
- elsif type == 'select'
= form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
- elsif type == 'password'
- = form.password_field name, value: value, class: 'form-control'
+ = form.password_field name, autocomplete: "new-password", class: 'form-control'
- if help
%span.help-block= help
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index c0a9923348e..67072b9fc2a 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -23,7 +23,7 @@
%li It will change the git path to repositories under this group.
.form-group.group-description-holder
- = f.label :description, 'Details', class: 'control-label'
+ = f.label :description, class: 'control-label'
.col-sm-10
= f.text_area :description, maxlength: 250,
class: 'form-control js-gfm-input', rows: 4
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
new file mode 100644
index 00000000000..285af56ad73
--- /dev/null
+++ b/app/views/shared/_import_form.html.haml
@@ -0,0 +1,16 @@
+.form-group.import-url-data
+ = f.label :import_url, class: 'control-label' do
+ %span Git repository URL
+ .col-sm-10
+ = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
+
+ .well.prepend-top-20
+ %ul
+ %li
+ The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.
+ %li
+ If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
+ %li
+ The import will time out after 4 minutes. For big repositories, use a clone/push combination.
+ %li
+ To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}.
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 0dbb6a04393..4b4c9e9eabe 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -3,8 +3,10 @@
.panel.panel-default.panel-small
- project = group[0]
.panel-heading
- = link_to_project project
- = link_to 'show all', namespace_project_issues_path(project.namespace, project), class: 'pull-right'
+ = link_to project.name_with_namespace, namespace_project_issues_path(project.namespace, project)
+ - if can?(current_user, :create_issue, project)
+ .pull-right
+ = link_to 'New issue', new_namespace_project_issue_path(project.namespace, project)
%ul.well-list.issues-list
- group[1].each do |issue|
@@ -12,4 +14,3 @@
= paginate @issues, theme: "gitlab"
- else
.nothing-here-block No issues to show
-
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
new file mode 100644
index 00000000000..da49c48acd3
--- /dev/null
+++ b/app/views/shared/_logo.svg
@@ -0,0 +1,21 @@
+<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>
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index c02c5af008a..be17a511b26 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -3,8 +3,11 @@
.panel.panel-default.panel-small
- project = group[0]
.panel-heading
- = link_to_project project
- = link_to 'show all', namespace_project_merge_requests_path(project.namespace, project), class: 'pull-right'
+ = link_to project.name_with_namespace, namespace_project_merge_requests_path(project.namespace, project)
+ - if can?(current_user, :create_merge_request, project)
+ .pull-right
+ = link_to 'New merge request', new_namespace_project_merge_request_path(project.namespace, project)
+
%ul.well-list.mr-list
- group[1].each do |merge_request|
= render 'projects/merge_requests/merge_request', merge_request: merge_request
diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml
new file mode 100644
index 00000000000..b8eef15fbec
--- /dev/null
+++ b/app/views/shared/_milestone_expired.html.haml
@@ -0,0 +1,5 @@
+- if milestone.expired? and not milestone.closed?
+ %span.cred (Expired)
+- if milestone.expires_at
+ %span
+ = milestone.expires_at
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
new file mode 100644
index 00000000000..31b02ed93d0
--- /dev/null
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -0,0 +1,17 @@
+= render 'shared/commit_message_container', placeholder: placeholder
+
+- unless @project.empty_repo?
+ .form-group.branch
+ = label_tag 'new_branch', 'Target branch', class: 'control-label'
+ .col-sm-10
+ = text_field_tag 'new_branch', @new_branch || @ref, required: true, class: "form-control js-new-branch"
+
+ .form-group.js-create-merge-request-form-group
+ .col-sm-offset-2.col-sm-10
+ .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 this commit
+
+ = hidden_field_tag 'original_branch', @ref, class: 'js-original-branch'
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
new file mode 100644
index 00000000000..960ff00b49d
--- /dev/null
+++ b/app/views/shared/_project_limit.html.haml
@@ -0,0 +1,8 @@
+- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project?
+ .project-limit-message.alert.alert-warning.hidden-xs
+ You won't be able to create new projects because you have reached your project limit.
+
+ .pull-right
+ = link_to "Don't show again", profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link'
+ |
+ = link_to 'Remind later', '#', class: 'hide-project-limit-message alert-link'
diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml
index cba18c14568..2aa46662613 100644
--- a/app/views/shared/issuable/_context.html.haml
+++ b/app/views/shared/issuable/_context.html.haml
@@ -1,5 +1,5 @@
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
- %div.prepend-top-20
+ %div.prepend-top-default
.issuable-context-title
%label
Assignee:
@@ -9,9 +9,9 @@
none
.issuable-context-selectbox
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true)
+ = 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)
- %div.prepend-top-20.clearfix
+ %div.prepend-top-default.clearfix
.issuable-context-title
%label
Milestone:
@@ -25,26 +25,33 @@
none
.issuable-context-selectbox
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- = f.select(:milestone_id, milestone_options(issuable), { include_blank: 'Select milestone' }, {class: 'select2 select2-compact js-select2 js-milestone'})
+ = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }})
= hidden_field_tag :issuable_context
= f.submit class: 'btn hide'
+ - if issuable.labels.any?
+ %div.prepend-top-default.clearfix
+ .issuable-context-title
+ %label Labels
+ .issuable-show-labels
+ - issuable.labels.each do |label|
+ = link_to_label(label)
+
- if current_user
- subscribed = issuable.subscribed?(current_user)
- %div.prepend-top-20.clearfix
+ %div.prepend-top-default.clearfix
.issuable-context-title
- %label
- Subscription:
- %button.btn.btn-block.subscribe-button{:type => 'button'}
- = icon('eye')
- %span= subscribed ? 'Unsubscribe' : 'Subscribe'
+ %label Subscription
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
.subscription-status{data: {status: subscribtion_status}}
.description-block.unsubscribed{class: ( 'hidden' if subscribed )}
You're not receiving notifications from this thread.
.description-block.subscribed{class: ( 'hidden' unless subscribed )}
You're receiving notifications because you're subscribed to this thread.
+ %button.btn.btn-block.subscribe-button{:type => 'button'}
+ = icon('eye')
+ %span= subscribed ? 'Unsubscribe' : 'Subscribe'
-:coffeescript
- new Subscription("#{toggle_subscription_path(issuable)}")
- new IssuableContext()
+:javascript
+ new Subscription("#{toggle_subscription_path(issuable)}");
+ new IssuableContext();
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 8f16773077e..ac6c248ccf1 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -31,22 +31,21 @@
.issues-other-filters
.filter-item.inline
= users_select_tag(:assignee_id, selected: params[:assignee_id],
- placeholder: 'Assignee', class: 'trigger-submit', any_user: true, null_user: true, first_user: true, current_user: true)
+ placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true)
.filter-item.inline
= users_select_tag(:author_id, selected: params[:author_id],
- placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true, current_user: true)
+ placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
.filter-item.inline.milestone-filter
= select_tag('milestone_title', projects_milestones_options,
class: 'select2 trigger-submit', include_blank: true,
data: {placeholder: 'Milestone'})
- - if @project
- .filter-item.inline.labels-filter
- = select_tag('label_name', project_labels_options(@project),
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Label'})
+ .filter-item.inline.labels-filter
+ = select_tag('label_name', projects_labels_options,
+ class: 'select2 trigger-submit', include_blank: true,
+ data: {placeholder: 'Label'})
.pull-right
= render 'shared/sort_dropdown'
@@ -54,16 +53,20 @@
- if controller.controller_name == 'issues'
.issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
- = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control')
- = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true)
- = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone")
+ .filter-item.inline
+ = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), include_blank: true, data: { placeholder: "Status" })
+ .filter-item.inline
+ = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true)
+ .filter-item.inline
+ = select_tag('update[milestone_id]', bulk_update_milestone_options, include_blank: true, data: { placeholder: "Milestone" })
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event]
- = button_tag "Update issues", class: "btn update_selected_issues btn-save"
-
-:coffeescript
- new UsersSelect()
+ .filter-item.inline
+ = button_tag "Update issues", class: "btn update_selected_issues btn-save"
- $('form.filter-form').on 'submit', (event) ->
- event.preventDefault()
- Turbolinks.visit @.action + '&' + $(@).serialize()
+:javascript
+ new UsersSelect();
+ $('form.filter-form').on('submit', function (event) {
+ event.preventDefault();
+ Turbolinks.visit(this.action + '&' + $(this).serialize());
+ });
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 33ec726e93c..91ccd1ef660 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -6,11 +6,10 @@
%span= msg
%br
.form-group
- = f.label :title, class: 'control-label' do
- %strong= 'Title *'
+ = f.label :title, class: 'control-label'
.col-sm-10
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
- class: 'form-control pad js-gfm-input', required: true
+ class: 'form-control pad js-gfm-input js-quick-submit', required: true
- if issuable.is_a?(MergeRequest)
%p.help-block
@@ -26,40 +25,29 @@
= 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'
- .col-sm-12.hint
- .pull-left
- Parsed with
- #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}.
- .pull-right
- Attach files by dragging &amp; dropping
- or #{link_to 'selecting them', '#', class: 'markdown-selector' }.
-
+ classes: 'description form-control js-quick-submit'
+ = render 'projects/notes/hints'
.clearfix
.error-alert
- %hr
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
+ %hr
.form-group
.issue-assignee
- = f.label :assignee_id, class: 'control-label' do
- %i.fa.fa-user
- Assign to
+ = f.label :assignee_id, "Assignee", class: 'control-label'
.col-sm-10
= users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]",
- placeholder: 'Select a user', class: 'custom-form-control', null_user: true,
+ placeholder: 'Select assignee', class: 'custom-form-control', null_user: true,
selected: issuable.assignee_id, project: @target_project || @project,
- first_user: true, current_user: true)
+ first_user: true, current_user: true, include_blank: true)
&nbsp;
= link_to 'Assign to me', '#', class: 'btn assign-to-me-link'
.form-group
.issue-milestone
- = f.label :milestone_id, class: 'control-label' do
- %i.fa.fa-clock-o
- Milestone
+ = f.label :milestone_id, "Milestone", class: 'control-label'
.col-sm-10
- if milestone_options(issuable).present?
= f.select(:milestone_id, milestone_options(issuable),
- { include_blank: 'Select milestone' }, { class: 'select2' })
+ { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } })
- else
.prepend-top-10
%span.light No open milestones available.
@@ -67,13 +55,11 @@
- if can? current_user, :admin_milestone, issuable.project
= link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank
.form-group
- = f.label :label_ids, class: 'control-label' do
- %i.fa.fa-tag
- Labels
+ = f.label :label_ids, "Labels", class: 'control-label'
.col-sm-10
- if issuable.project.labels.any?
= f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
- { selected: issuable.label_ids }, multiple: true, class: 'select2'
+ { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
- else
.prepend-top-10
%span.light No labels yet.
@@ -85,31 +71,30 @@
%hr
- if @merge_request.new_record?
.form-group
- = f.label :source_branch, class: 'control-label' do
- %i.fa.fa-code-fork
- Source Branch
+ = f.label :source_branch, class: 'control-label'
.col-sm-10
= f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
.form-group
- = f.label :target_branch, class: 'control-label' do
- %i.fa.fa-code-fork
- Target Branch
+ = f.label :target_branch, class: 'control-label'
.col-sm-10
- = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record? })
+ = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} })
- if @merge_request.new_record?
%p.help-block
= link_to 'Change branches', mr_change_branches_path(@merge_request)
-.form-actions
- - if !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) && !issuable.persisted?
- %p
- Please review the
- %strong #{link_to 'guidelines for contribution', guide_url}
- to this repository.
+- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
+.gray-content-block{class: (is_footer ? "footer-block" : "middle-block")}
- if issuable.new_record?
- = f.submit "Submit new #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
+ = f.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
- else
= f.submit 'Save changes', class: 'btn btn-save'
+
+ - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project))
+ .inline.prepend-left-10
+ Please review the
+ %strong #{link_to 'contribution guidelines', guide_url}
+ for this project.
+
- if issuable.new_record?
- cancel_project = issuable.source_project
- else
diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml
index 58c3de64b77..3a5ad00aa91 100644
--- a/app/views/shared/issuable/_search_form.html.haml
+++ b/app/views/shared/issuable/_search_form.html.haml
@@ -1,6 +1,6 @@
= 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' }
+ = 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']
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 16e1d8421de..e5ffe1e29ae 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -2,11 +2,12 @@
- avatar = true unless local_assigns[:avatar] == false
- stars = true unless local_assigns[:stars] == false
- ci = false unless local_assigns[:ci] == true
+- skip_namespace = false unless local_assigns[:skip_namespace] == true
%ul.projects-list
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) ? 'hide' : nil
- = render "shared/projects/project", project: project,
+ = 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
@@ -16,5 +17,5 @@
= link_to '#', class: 'js-expand' do
Show all
-:coffeescript
- new ProjectsList()
+:javascript
+ new ProjectsList();
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index e67e5a8a638..c36995b94d7 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -1,6 +1,7 @@
- avatar = true unless local_assigns[:avatar] == false
- stars = true unless local_assigns[:stars] == false
- 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?
%li.project-row{ class: css_class }
@@ -11,7 +12,7 @@
= project_icon(project, alt: '', class: 'avatar project-avatar s46')
%span.project-full-name
%span.namespace-name
- - if project.namespace
+ - if project.namespace && !skip_namespace
= project.namespace.human_name
\/
%span.project-name.filter-title
@@ -20,9 +21,7 @@
.project-controls
- if ci && !project.empty_repo? && project.commit
- if ci_commit = project.ci_commit(project.commit.sha)
- = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}",
- title: "Build status: #{ci_commit.status}", data: {toggle: 'tooltip', placement: 'left'} do
- = ci_status_icon(ci_commit)
+ = render_ci_status(ci_commit)
&nbsp;
- if stars
%span
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 913b6744844..1041eccd1df 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,5 +1,5 @@
.snippet-form-holder
- = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form" } do |f|
+ = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
- if @snippet.errors.any?
.alert.alert-danger
%ul
@@ -8,7 +8,8 @@
.form-group
= f.label :title, class: 'control-label'
- .col-sm-10= f.text_field :title, placeholder: "Example Snippet", class: 'form-control', required: true
+ .col-sm-10
+ = f.text_field :title, class: 'form-control', required: true, autofocus: true
= render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet
@@ -27,7 +28,7 @@
- if @snippet.new_record?
= f.submit 'Create snippet', class: "btn-create btn"
- else
- = f.submit 'Save', class: "btn-save btn"
+ = f.submit 'Save changes', class: "btn-save btn"
- if @snippet.project_id
= link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
new file mode 100644
index 00000000000..89c1d7122b0
--- /dev/null
+++ b/app/views/shared/snippets/_header.html.haml
@@ -0,0 +1,25 @@
+.issuable-details
+ .page-title
+ .snippet-box.has_tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level), data: { container: 'body' }}
+ = visibility_level_icon(@snippet.visibility_level, fw: false)
+ = visibility_level_label(@snippet.visibility_level)
+ Snippet ##{@snippet.id}
+ %span.creator
+ &middot; created by #{link_to_member(@project, @snippet.author, size: 24)}
+ &middot;
+ = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
+ - if @snippet.updated_at != @snippet.created_at
+ %span
+ &middot;
+ = icon('edit', title: 'edited')
+ = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago')
+
+ .pull-right
+ - if @snippet.project_id?
+ = render "projects/snippets/actions"
+ - else
+ = render "snippets/actions"
+
+ .gray-content-block.middle-block
+ %h2.issue-title
+ = gfm escape_once(@snippet.title)
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 69a713ad9aa..c6294caddc7 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -18,4 +18,3 @@
= image_tag avatar_icon(snippet.author_email), class: "avatar s24", alt: ''
= snippet.author_name
authored #{time_ago_with_tooltip(snippet.created_at)}
-
diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml
new file mode 100644
index 00000000000..cfd11e45b6a
--- /dev/null
+++ b/app/views/sherlock/file_samples/show.html.haml
@@ -0,0 +1,55 @@
+- page_title t('sherlock.title'), t('sherlock.transaction'),
+ t('sherlock.file_sample')
+
+- header_title t('sherlock.title'), sherlock_transactions_path
+
+.gray-content-block
+ .pull-right
+ = link_to(sherlock_transaction_path(@transaction), class: 'btn') do
+ %i.fa.fa-arrow-left
+ = t('sherlock.transaction')
+ .oneline
+ = t('sherlock.file_sample')
+ = @file_sample.id
+
+.prepend-top-default
+ %p
+ %span.light
+ #{t('sherlock.time')}:
+ %strong
+ = @file_sample.duration.round(2)
+ = t('sherlock.milliseconds')
+ %p
+ %span.light
+ #{t('sherlock.events')}:
+ %strong
+ = @file_sample.events
+
+%article.file-holder
+ .file-title
+ %i.fa.fa-file-text-o.fa-fw
+ %strong
+ = @file_sample.file
+ .code.file-content.js-syntax-highlight
+ .line-numbers
+ %table.sherlock-line-samples-table
+ %thead
+ %tr
+ %th= t('sherlock.line_capitalized')
+ %th= t('sherlock.events')
+ %th= t('sherlock.time')
+ %th= t('sherlock.percent')
+ %tbody
+ - @file_sample.line_samples.each_with_index do |sample, index|
+ %tr{class: sample.majority_of?(@file_sample.duration) ? 'slow' : ''}
+ %td= index + 1
+ %td= sample.events
+ %td
+ = sample.duration.round(2)
+ = t('sherlock.milliseconds')
+ %td
+ = sample.percentage_of(@file_sample.duration).round
+ = t('sherlock.percent')
+
+ .sherlock-file-sample
+ = highlight(@file_sample.file, @file_sample.source)
diff --git a/app/views/sherlock/queries/_backtrace.html.haml b/app/views/sherlock/queries/_backtrace.html.haml
new file mode 100644
index 00000000000..5c9294c0ab5
--- /dev/null
+++ b/app/views/sherlock/queries/_backtrace.html.haml
@@ -0,0 +1,27 @@
+.prepend-top-default
+ .panel.panel-default
+ .panel-heading
+ %strong
+ = t('sherlock.application_backtrace')
+ %ul.well-list
+ - @query.application_backtrace.each do |location|
+ %li
+ = location.path
+ %small.light
+ = t('sherlock.line')
+ = location.line
+
+ .panel.panel-default
+ .panel-heading
+ %strong
+ = t('sherlock.full_backtrace')
+ %ul.well-list
+ - @query.backtrace.each do |location|
+ %li
+ - if location.application?
+ %strong= location.path
+ - else
+ = location.path
+ %small.light
+ = t('sherlock.line')
+ = location.line
diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml
new file mode 100644
index 00000000000..549b47430e6
--- /dev/null
+++ b/app/views/sherlock/queries/_general.html.haml
@@ -0,0 +1,50 @@
+.prepend-top-default
+ .panel.panel-default
+ .panel-heading
+ %strong
+ = t('sherlock.general')
+ %ul.well-list
+ %li
+ %span.light
+ #{t('sherlock.time')}:
+ %strong
+ = @query.duration.round(4)
+ = t('sherlock.milliseconds')
+ %li
+ %span.light
+ #{t('sherlock.origin')}:
+ %strong
+ = @query.last_application_frame.path
+ %small.light
+ = t('sherlock.line')
+ = @query.last_application_frame.line
+
+ .panel.panel-default
+ .panel-heading
+ .pull-right
+ %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button}
+ %i.fa.fa-clipboard
+ %pre.hidden
+ = @query.formatted_query
+ %strong
+ = t('sherlock.query')
+ %ul.well-list
+ %li
+ .code.js-syntax-highlight.sherlock-code
+ :preserve
+ #{highlight("#{@query.id}.sql", @query.formatted_query)}
+
+ .panel.panel-default
+ .panel-heading
+ .pull-right
+ %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button}
+ %i.fa.fa-clipboard
+ %pre.hidden
+ = @query.explain
+ %strong
+ = t('sherlock.query_plan')
+ %ul.well-list
+ %li
+ .code.js-syntax-highlight.sherlock-code
+ %pre
+ %code= @query.explain
diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml
new file mode 100644
index 00000000000..4a84348ac82
--- /dev/null
+++ b/app/views/sherlock/queries/show.html.haml
@@ -0,0 +1,26 @@
+- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query')
+- header_title t('sherlock.title'), sherlock_transactions_path
+
+%ul.center-top-menu
+ %li.active
+ %a(href="#tab-general" data-toggle="tab")
+ = t('sherlock.general')
+ %li
+ %a(href="#tab-backtrace" data-toggle="tab")
+ = t('sherlock.backtrace')
+
+.gray-content-block
+ .pull-right
+ = link_to(sherlock_transaction_path(@transaction), class: 'btn') do
+ %i.fa.fa-arrow-left
+ = t('sherlock.transaction')
+ .oneline
+ = t('sherlock.query')
+ = @query.id
+
+.tab-content
+ .tab-pane.active#tab-general
+ = render(partial: 'general')
+
+ .tab-pane#tab-backtrace
+ = render(partial: 'backtrace')
diff --git a/app/views/sherlock/transactions/_file_samples.html.haml b/app/views/sherlock/transactions/_file_samples.html.haml
new file mode 100644
index 00000000000..4349c9b7ace
--- /dev/null
+++ b/app/views/sherlock/transactions/_file_samples.html.haml
@@ -0,0 +1,24 @@
+- if @transaction.file_samples.empty?
+ .nothing-here-block
+ = t('sherlock.no_file_samples')
+- else
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th= t('sherlock.time_inclusive')
+ %th= t('sherlock.count')
+ %th= t('sherlock.path')
+ %th
+ %tbody
+ - @transaction.sorted_file_samples.each do |sample|
+ %tr
+ %td
+ = sample.duration.round(2)
+ = t('sherlock.milliseconds')
+ %td= @transaction.view_counts.fetch(sample.file, 1)
+ %td= sample.relative_path
+ %td
+ = link_to(t('sherlock.view'),
+ sherlock_transaction_file_sample_path(@transaction, sample),
+ class: 'btn btn-xs')
diff --git a/app/views/sherlock/transactions/_general.html.haml b/app/views/sherlock/transactions/_general.html.haml
new file mode 100644
index 00000000000..8533b130da6
--- /dev/null
+++ b/app/views/sherlock/transactions/_general.html.haml
@@ -0,0 +1,39 @@
+.prepend-top-default
+ .panel.panel-default
+ .panel-heading
+ %strong
+ = t('sherlock.general')
+ %ul.well-list
+ %li
+ %span.light
+ #{t('sherlock.id')}:
+ %strong
+ = @transaction.id
+ %li
+ %span.light
+ #{t('sherlock.type')}:
+ %strong
+ = @transaction.type
+ %li
+ %span.light
+ #{t('sherlock.path')}:
+ %strong
+ = @transaction.path
+ %li
+ %span.light
+ #{t('sherlock.time')}:
+ %strong
+ = @transaction.duration.round(2)
+ = t('sherlock.seconds')
+ %li
+ %span.light
+ #{t('sherlock.query_time')}
+ %strong
+ = @transaction.query_duration.round(2)
+ = t('sherlock.seconds')
+ %li
+ %span.light
+ #{t('sherlock.finished_at')}:
+ %strong
+ = time_ago_in_words(@transaction.finished_at)
+ = t('sherlock.ago')
diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml
new file mode 100644
index 00000000000..b7e0162e80d
--- /dev/null
+++ b/app/views/sherlock/transactions/_queries.html.haml
@@ -0,0 +1,24 @@
+- if @transaction.queries.empty?
+ .nothing-here-block
+ = t('sherlock.no_queries')
+- else
+ .table-holder
+ %table.table#sherlock-queries
+ %thead
+ %tr
+ %th= t('sherlock.time')
+ %th= t('sherlock.query')
+ %td
+ %tbody
+ - @transaction.sorted_queries.each do |query|
+ %tr
+ %td
+ = query.duration.round(2)
+ = t('sherlock.milliseconds')
+ %td
+ .code.js-syntax-highlight.sherlock-code
+ = highlight("#{query.id}.sql", query.formatted_query)
+ %td
+ = link_to(t('sherlock.view'),
+ sherlock_transaction_query_path(@transaction, query),
+ class: 'btn btn-xs')
diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml
new file mode 100644
index 00000000000..010e1a2a902
--- /dev/null
+++ b/app/views/sherlock/transactions/index.html.haml
@@ -0,0 +1,42 @@
+- page_title t('sherlock.title')
+- header_title t('sherlock.title'), sherlock_transactions_path
+
+.gray-content-block
+ .pull-right
+ = link_to(destroy_all_sherlock_transactions_path,
+ class: 'btn btn-danger',
+ method: :delete) do
+ %i.fa.fa-trash
+ = t('sherlock.delete_all_transactions')
+ .oneline= t('sherlock.introduction')
+
+- if @transactions.empty?
+ .nothing-here-block= t('sherlock.no_transactions')
+- else
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th= t('sherlock.type')
+ %th= t('sherlock.path')
+ %th= t('sherlock.time')
+ %th= t('sherlock.queries')
+ %th= t('sherlock.finished_at')
+ %th
+ %tbody
+ - @transactions.each do |trans|
+ %tr
+ %td= trans.type
+ %td
+ %span{title: trans.path}
+ = truncate(trans.path, length: 70)
+ %td
+ = trans.duration.round(2)
+ = t('sherlock.seconds')
+ %td= trans.queries.length
+ %td
+ = time_ago_in_words(trans.finished_at)
+ = t('sherlock.ago')
+ %td
+ = link_to(sherlock_transaction_path(trans), class: 'btn btn-xs') do
+ = t('sherlock.view')
diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml
new file mode 100644
index 00000000000..3c8ffb06648
--- /dev/null
+++ b/app/views/sherlock/transactions/show.html.haml
@@ -0,0 +1,36 @@
+- page_title t('sherlock.title'), t('sherlock.transaction')
+- header_title t('sherlock.title'), sherlock_transactions_path
+
+%ul.center-top-menu
+ %li.active
+ %a(href="#tab-general" data-toggle="tab")
+ = t('sherlock.general')
+ %li
+ %a(href="#tab-queries" data-toggle="tab")
+ = t('sherlock.queries')
+ %span.badge
+ #{@transaction.queries.length}
+ %li
+ %a(href="#tab-file-samples" data-toggle="tab")
+ = t('sherlock.file_samples')
+ %span.badge
+ #{@transaction.file_samples.length}
+
+.gray-content-block
+ .pull-right
+ = link_to(sherlock_transactions_path, class: 'btn') do
+ %i.fa.fa-arrow-left
+ = t('sherlock.all_transactions')
+ .oneline
+ = t('sherlock.transaction')
+ = @transaction.id
+
+.tab-content
+ .tab-pane.active#tab-general
+ = render(partial: 'general')
+
+ .tab-pane#tab-queries
+ = render(partial: 'queries')
+
+ .tab-pane#tab-file-samples
+ = render(partial: 'file_samples')
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
new file mode 100644
index 00000000000..1979ae6d5bc
--- /dev/null
+++ b/app/views/snippets/_actions.html.haml
@@ -0,0 +1,11 @@
+= link_to new_snippet_path, class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do
+ = icon('plus')
+ New Snippet
+- if can?(current_user, :update_personal_snippet, @snippet)
+ = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
+ = icon('pencil-square-o')
+ Edit
+- if can?(current_user, :admin_personal_snippet, @snippet)
+ = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do
+ = icon('trash-o')
+ Delete
diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml
index 1a380035661..82f44a9a5c3 100644
--- a/app/views/snippets/edit.html.haml
+++ b/app/views/snippets/edit.html.haml
@@ -1,5 +1,5 @@
- page_title "Edit", @snippet.title, "Snippets"
%h3.page-title
- Edit snippet
+ Edit Snippet
%hr
= render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level
diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml
index a74d5e792ad..79e2392490d 100644
--- a/app/views/snippets/new.html.haml
+++ b/app/views/snippets/new.html.haml
@@ -1,5 +1,5 @@
- page_title "New Snippet"
%h3.page-title
- New snippet
+ New Snippet
%hr
= render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 97374e073dc..69d8899d4c1 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,41 +1,14 @@
- page_title @snippet.title, "Snippets"
-%h4.page-title
- = @snippet.title
- - if @snippet.private?
- %span.label.label-success
- %i.fa.fa-lock
- private
-
- .pull-right
- = link_to new_snippet_path, class: "btn btn-new btn-sm", title: "New Snippet" do
- Add new snippet
-
-.append-bottom-10.prepend-top-10
- .pull-right
- %span.light
- created by
- = link_to user_snippets_path(@snippet.author) do
- = @snippet.author_name
-
- .back-link
- - if @snippet.author == current_user
- = link_to dashboard_snippets_path do
- &larr; your snippets
- - else
- = link_to explore_snippets_path do
- &larr; explore snippets
-
-.file-holder
- .file-title
- %i.fa.fa-file
- %strong
- = @snippet.file_name
- .file-actions
- .btn-group
- - if can?(current_user, :update_personal_snippet, @snippet)
- = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-sm", title: 'Edit Snippet'
- = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
- - if can?(current_user, :admin_personal_snippet, @snippet)
- = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet'
- = render 'shared/snippets/blob'
+.snippet-holder
+ = render 'shared/snippets/header'
+
+ %article.file-holder
+ .file-title
+ = blob_icon 0, @snippet.file_name
+ %strong
+ = @snippet.file_name
+ .file-actions.hidden-xs
+ .btn-group.tree-btn-group
+ = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
+ = render 'shared/snippets/blob'
diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml
index 922b0c6cebf..7f29918dba3 100644
--- a/app/views/users/calendar.html.haml
+++ b/app/views/users/calendar.html.haml
@@ -1,7 +1,3 @@
-%h4
- Contributions calendar
- .pull-right
- %small Issues, merge requests and push events
#cal-heatmap.calendar
:javascript
new Calendar(
@@ -10,3 +6,5 @@
#{@starting_month},
'#{user_calendar_activities_path}'
);
+
+.calendar-hint Summary of issues, merge requests and push events
diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder
index 50232dc7186..2fe5b7fac83 100644
--- a/app/views/users/show.atom.builder
+++ b/app/views/users/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml"
xml.link href: user_url(@user), rel: "alternate", type: "text/html"
xml.id user_url(@user)
- xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 37d5dba0330..d5a92cb816a 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -6,62 +6,114 @@
= render 'shared/show_aside'
-.row
- %section.col-md-7
- .header-with-avatar
- = link_to avatar_icon(@user.email, 400), target: '_blank' do
- = image_tag avatar_icon(@user.email, 90), class: "avatar avatar-tile s90", alt: ''
- %h3
- = @user.name
- - if @user == current_user
- .pull-right.hidden-xs
- = link_to profile_path, class: 'btn btn-sm' do
- = icon('user')
- Profile settings
- - elsif current_user
- .pull-right
- %span.dropdown
- %a.light.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"}
- = icon('exclamation-circle')
- %ul.dropdown-menu.dropdown-menu-right
- %li
- = link_to new_abuse_report_path(user_id: @user.id) do
- Report abuse
+.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
- .username
- @#{@user.username}
- .description
- - if @user.bio.present?
- = @user.bio
+ .cover-desc
+ %span
+ @#{@user.username}.
+ - if @user.bio.present?
+ %span
+ #{@user.bio}.
+ %span
+ Member since #{@user.created_at.stamp("Aug 21, 2011")}
- .clearfix
+ .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
- - if @groups.any?
- .prepend-top-20
- %h4 Groups
- = render 'groups', groups: @groups
- %hr
- .hidden-xs
- .user-calendar
- %h4.center.light
- %i.fa.fa-spinner.fa-spin
- .user-calendar-activities
- %hr
- %h4
- User Activity
+ .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')
- - if current_user
- %span.rss-icon.pull-right
- = link_to user_path(@user, :atom, { private_token: current_user.private_token }) do
- %strong
- %i.fa.fa-rss
+.gray-content-block.second-block
+ .user-calendar
+ %h4.center.light
+ %i.fa.fa-spinner.fa-spin
+ .user-calendar-activities
+
+%ul.center-middle-menu
+ %li.active
+ = link_to "#activity", 'data-toggle' => 'tab' do
+ Activity
+ - if @groups.any?
+ %li
+ = link_to "#groups", 'data-toggle' => 'tab' do
+ Groups
+ - if @contributed_projects.present?
+ %li
+ = link_to "#contributed", 'data-toggle' => 'tab' do
+ Contributed projects
+ - if @projects.present?
+ %li
+ = link_to "#personal", 'data-toggle' => 'tab' do
+ Personal projects
+
+.tab-content
+ .tab-pane.active#activity
.content_list
= spinner
- %aside.col-md-5
- = render 'profile', user: @user
- = render 'projects', projects: @projects, contributed_projects: @contributed_projects
-:coffeescript
- $(".user-calendar").load("#{user_calendar_path}")
+ - if @groups.any?
+ .tab-pane#groups
+ %ul.content-list
+ - @groups.each do |group|
+ = render 'shared/groups/group', group: group
+
+ - if @contributed_projects.present?
+ .tab-pane#contributed
+ .contributed-projects
+ = render 'shared/projects/list',
+ projects: @contributed_projects.sort_by(&:star_count).reverse,
+ projects_limit: 5, stars: true, avatar: true
+
+ - if @projects.present?
+ .tab-pane#personal
+ .personal-projects
+ = render 'shared/projects/list',
+ projects: @projects.sort_by(&:star_count).reverse,
+ projects_limit: 10, stars: true, avatar: true
+
+:javascript
+ $(".user-calendar").load("#{user_calendar_path}");
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
index 36ea6742064..7eb27c12d33 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -1,10 +1,32 @@
-.votes.votes-block
- .btn-group
- - unless votable.upvotes.zero?
- .btn.btn-sm.disabled.cgreen
- %i.fa.fa-thumbs-up
- = votable.upvotes
- - unless votable.downvotes.zero?
- .btn.btn-sm.disabled.cred
- %i.fa.fa-thumbs-down
- = votable.downvotes
+.awards.votes-block
+ - votable.notes.awards.grouped_awards.each do |emoji, notes|
+ .award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)}
+ .icon{"data-emoji" => "#{emoji}"}
+ = image_tag url_to_emoji(emoji), height: "20px", width: "20px"
+ .counter
+ = notes.count
+
+ - if current_user
+ .dropdown.awards-controls
+ %a.add-award{"data-toggle" => "dropdown", "data-target" => "#", "href" => "#"}
+ = icon('smile-o')
+ %ul.dropdown-menu.awards-menu
+ - emoji_list.each do |emoji|
+ %li{"data-emoji" => "#{emoji}"}= image_tag url_to_emoji(emoji), height: "20px", width: "20px"
+
+- 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}"
+ window.awards_handler = new AwardsHandler(post_emoji_url, noteable_type, noteable_id)
+
+ $(".awards-menu li").click (e)->
+ emoji = $(this).data("emoji")
+ awards_handler.addAward(emoji)
+
+ $(".awards").on "click", ".award", (e)->
+ emoji = $(this).find(".icon").data("emoji")
+ awards_handler.addAward(emoji)
+
+ $(".award").tooltip()
diff --git a/app/views/votes/_votes_inline.html.haml b/app/views/votes/_votes_inline.html.haml
deleted file mode 100644
index 2cb3ae04e1a..00000000000
--- a/app/views/votes/_votes_inline.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-.votes.votes-inline
- - unless votable.upvotes.zero?
- %span.upvotes.cgreen
- + #{votable.upvotes}
- - unless votable.downvotes.zero?
- \/
- - unless votable.downvotes.zero?
- %span.downvotes.cred
- \- #{votable.downvotes}
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 5a921a73fe9..f2649e38eb3 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -46,6 +46,6 @@ class EmailReceiverWorker
return
end
- EmailRejectionMailer.delay.rejection(reason, raw, can_retry)
+ EmailRejectionMailer.rejection(reason, raw, can_retry).deliver_later
end
end
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 916a99bb273..c4d8595d45d 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -53,7 +53,7 @@ class EmailsOnPushWorker
reverse_compare: reverse_compare,
send_from_committer_email: send_from_committer_email,
disable_diffs: disable_diffs
- ).deliver
+ ).deliver_now
# These are input errors and won't be corrected even if Sidekiq retries
rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
logger.info("Failed to send e-mail for project '#{project.name_with_namespace}' to #{recipient}: #{e}")
diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb
new file mode 100644
index 00000000000..47c5a670ed4
--- /dev/null
+++ b/app/workers/repository_archive_cache_worker.rb
@@ -0,0 +1,9 @@
+class RepositoryArchiveCacheWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform
+ Repository.clean_old_archives
+ end
+end
diff --git a/app/workers/repository_archive_worker.rb b/app/workers/repository_archive_worker.rb
deleted file mode 100644
index 021c1139568..00000000000
--- a/app/workers/repository_archive_worker.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-class RepositoryArchiveWorker
- include Sidekiq::Worker
-
- sidekiq_options queue: :archive_repo
-
- attr_accessor :project, :ref, :format
-
- def perform(project_id, ref, format)
- @project = Project.find(project_id)
- @ref, @format = ref, format.downcase
-
- repository = project.repository
-
- repository.clean_old_archives
-
- return unless file_path
- return if archived? || archiving?
-
- repository.archive_repo(ref, storage_path, format)
- end
-
- private
-
- def storage_path
- Gitlab.config.gitlab.repository_downloads_path
- end
-
- def file_path
- @file_path ||= project.repository.archive_file_path(ref, storage_path, format)
- end
-
- def pid_file_path
- @pid_file_path ||= project.repository.archive_pid_file_path(ref, storage_path, format)
- end
-
- def archived?
- File.exist?(file_path)
- end
-
- def archiving?
- File.exist?(pid_file_path)
- end
-end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index acd1c43f06b..2f991c52339 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -13,22 +13,20 @@ class RepositoryForkWorker
end
result = gitlab_shell.fork_repository(source_path, target_path)
-
unless result
logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}")
+ project.update(import_error: "The project could not be forked.")
project.import_fail
- project.save
return
end
- if project.valid_repo?
- ProjectCacheWorker.perform_async(project.id)
- project.import_finish
- else
- project.import_fail
+ unless project.valid_repo?
logger.error("Project #{id} had an invalid repository after fork")
+ project.update(import_error: "The forked repository is invalid.")
+ project.import_fail
+ return
end
- project.save
+ project.import_finish
end
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index ea2808045eb..d18c0706b30 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -7,37 +7,49 @@ class RepositoryImportWorker
def perform(project_id)
project = Project.find(project_id)
- unless project.import_url == Project::UNKNOWN_IMPORT_URL
- import_result = gitlab_shell.send(:import_repository,
- project.path_with_namespace,
- project.import_url)
- return project.import_fail unless import_result
- else
+ if project.import_url == Project::UNKNOWN_IMPORT_URL
+ # In this case, we only want to import issues, not a repository.
unless project.create_repository
- return project.import_fail
+ project.update(import_error: "The repository could not be created.")
+ project.import_fail
+ return
+ end
+ else
+ begin
+ gitlab_shell.import_repository(project.path_with_namespace, project.import_url)
+ rescue Gitlab::Shell::Error => e
+ project.update(import_error: e.message)
+ project.import_fail
+ return
end
end
- data_import_result = case project.import_type
- when 'github'
- Gitlab::GithubImport::Importer.new(project).execute
- when 'gitlab'
- Gitlab::GitlabImport::Importer.new(project).execute
- when 'bitbucket'
- Gitlab::BitbucketImport::Importer.new(project).execute
- when 'google_code'
- Gitlab::GoogleCodeImport::Importer.new(project).execute
- when 'fogbugz'
- Gitlab::FogbugzImport::Importer.new(project).execute
- else
- true
- end
- return project.import_fail unless data_import_result
+ data_import_result =
+ case project.import_type
+ when 'github'
+ Gitlab::GithubImport::Importer.new(project).execute
+ when 'gitlab'
+ Gitlab::GitlabImport::Importer.new(project).execute
+ when 'bitbucket'
+ Gitlab::BitbucketImport::Importer.new(project).execute
+ when 'google_code'
+ Gitlab::GoogleCodeImport::Importer.new(project).execute
+ when 'fogbugz'
+ Gitlab::FogbugzImport::Importer.new(project).execute
+ else
+ true
+ end
- Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket'
+ unless data_import_result
+ project.update(import_error: "The remote issue data could not be imported.")
+ project.import_fail
+ return
+ end
+
+ if project.import_type == 'bitbucket'
+ Gitlab::BitbucketImport::KeyDeleter.new(project).execute
+ end
project.import_finish
- project.save
- ProjectCacheWorker.perform_async(project.id)
end
end
diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb
new file mode 100644
index 00000000000..ca594e77e7c
--- /dev/null
+++ b/app/workers/stuck_ci_builds_worker.rb
@@ -0,0 +1,18 @@
+class StuckCiBuildsWorker
+ include Sidekiq::Worker
+
+ BUILD_STUCK_TIMEOUT = 1.day
+
+ def perform
+ Rails.logger.info 'Cleaning stuck builds'
+
+ builds = Ci::Build.running_or_pending.where('updated_at < ?', BUILD_STUCK_TIMEOUT.ago)
+ builds.find_each(batch_size: 50).each do |build|
+ Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}"
+ build.drop
+ end
+
+ # Update builds that failed to drop
+ builds.update_all(status: 'failed')
+ end
+end
diff --git a/bin/background_jobs b/bin/background_jobs
index d4578f6a222..5c85fb339e6 100755
--- a/bin/background_jobs
+++ b/bin/background_jobs
@@ -37,7 +37,7 @@ start_no_deamonize()
start_sidekiq()
{
- bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1
+ bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1
}
load_ok()
diff --git a/bin/ci/upgrade.rb b/bin/ci/upgrade.rb
index aab4f60ec60..aab4f60ec60 100644..100755
--- a/bin/ci/upgrade.rb
+++ b/bin/ci/upgrade.rb
diff --git a/bin/rails b/bin/rails
index 7feb6a30e69..5191e6927af 100755
--- a/bin/rails
+++ b/bin/rails
@@ -1,8 +1,4 @@
#!/usr/bin/env ruby
-begin
- load File.expand_path("../spring", __FILE__)
-rescue LoadError
-end
-APP_PATH = File.expand_path('../../config/application', __FILE__)
+APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'
diff --git a/bin/rake b/bin/rake
index 0fb4e07e13a..17240489f64 100755
--- a/bin/rake
+++ b/bin/rake
@@ -1,7 +1,4 @@
#!/usr/bin/env ruby
-begin
- load File.expand_path("../spring", __FILE__)
-rescue LoadError
-end
-require 'bundler/setup'
-load Gem.bin_path('rake', 'rake')
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 00000000000..acdb2c1389c
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+require 'pathname'
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+Dir.chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file:
+
+ puts "== Installing dependencies =="
+ system "gem install bundler --conservative"
+ system "bundle check || bundle install"
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?("config/database.yml")
+ # system "cp config/database.yml.sample config/database.yml"
+ # end
+
+ puts "\n== Preparing database =="
+ system "bin/rake db:setup"
+
+ puts "\n== Removing old logs and tempfiles =="
+ system "rm -f log/*"
+ system "rm -rf tmp/cache"
+
+ puts "\n== Restarting application server =="
+ system "touch tmp/restart.txt"
+end
diff --git a/bin/upgrade.rb b/bin/upgrade.rb
index a5caecf8526..a5caecf8526 100644..100755
--- a/bin/upgrade.rb
+++ b/bin/upgrade.rb
diff --git a/config/application.rb b/config/application.rb
index a96e22211e6..d255ff0719f 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -74,7 +74,7 @@ module Gitlab
origins '*'
resource '/api/*',
headers: :any,
- methods: [:get, :post, :options, :put, :delete],
+ methods: :any,
expose: ['Link']
end
end
@@ -99,6 +99,10 @@ module Gitlab
redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
config.cache_store = :redis_store, redis_config_hash
+ config.active_record.raise_in_transactional_callbacks = true
+
+ config.active_job.queue_adapter = :sidekiq
+
# This is needed for gitlab-shell
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
end
diff --git a/config/environment.rb b/config/environment.rb
index 3b186a9d57a..df3006d349c 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -2,4 +2,4 @@
require File.expand_path('../application', __FILE__)
# Initialize the rails application
-Gitlab::Application.initialize!
+Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
index d7d6aed1602..c22722c606b 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,4 +1,4 @@
-Gitlab::Application.configure do
+Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb
# In the development environment your application's code is reloaded on
@@ -24,7 +24,7 @@ Gitlab::Application.configure do
# Expands the lines which load the assets
# config.assets.debug = true
-
+
# Adds additional error checking when serving assets at runtime.
# Checks for improperly declared sprockets dependencies.
# Raises helpful error messages.
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 3316ece3873..909526605a1 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -1,4 +1,4 @@
-Gitlab::Application.configure do
+Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb
# Code is not reloaded between requests
@@ -9,7 +9,7 @@ Gitlab::Application.configure do
config.action_controller.perform_caching = true
# Disable Rails's static asset server (Apache or nginx will already do this)
- config.serve_static_assets = false
+ config.serve_static_files = false
# Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier
@@ -32,7 +32,7 @@ Gitlab::Application.configure do
# config.force_ssl = true
# See everything in the log (default is :info)
- # config.log_level = :debug
+ config.log_level = :info
# Suppress 'Rendered template ...' messages in the log
# source: http://stackoverflow.com/a/16369363
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 2d5e7addcd3..f96ac6f9753 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,4 +1,4 @@
-Gitlab::Application.configure do
+Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb
# The test environment is used exclusively to run your application's
@@ -8,7 +8,7 @@ Gitlab::Application.configure do
config.cache_classes = false
# Configure static asset server for tests with Cache-Control for performance
- config.serve_static_assets = true
+ config.serve_static_files = true
config.static_cache_control = "public, max-age=3600"
# Show full error reports and disable caching
@@ -30,4 +30,8 @@ Gitlab::Application.configure do
config.active_support.deprecation = :stderr
config.eager_load = false
+
+ config.cache_store = :null_store
+
+ config.active_job.queue_adapter = :test
end
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 15930fc9079..1da42ab38f3 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -84,6 +84,7 @@ production: &base
merge_requests: true
wiki: true
snippets: false
+ builds: true
## Webhook settings
# Number of seconds to wait for HTTP response after sending webhook HTTP POST request (default: 10)
@@ -99,7 +100,41 @@ production: &base
# For documentation on how to set this up, see http://doc.gitlab.com/ce/incoming_email/README.html
incoming_email:
enabled: false
- address: "incoming+%{key}@gitlab.example.com"
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`.
+ address: "gitlab-incoming+%{key}@gmail.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "gitlab-incoming@gmail.com"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "imap.gmail.com"
+ # IMAP server port
+ port: 993
+ # Whether the IMAP server uses SSL
+ ssl: true
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
+
+ ## Build Artifacts
+ artifacts:
+ enabled: true
+ # The location where build artifacts are stored (default: shared/artifacts).
+ # path: shared/artifacts
+
+ ## Git LFS
+ lfs:
+ enabled: true
+ # The location where LFS objects are stored (default: shared/lfs-objects).
+ # storage_path: shared/lfs-objects
## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
@@ -159,7 +194,7 @@ production: &base
method: 'plain' # "tls" or "ssl" or "plain"
bind_dn: '_the_full_dn_of_the_user_you_will_bind_with'
password: '_the_password_of_the_bind_user'
-
+
# This setting specifies if LDAP server is Active Directory LDAP server.
# For non AD servers it skips the AD specific queries.
# If your LDAP server is not AD, set this to false.
@@ -204,13 +239,13 @@ production: &base
# The username will be used in paths for the user's own projects
# (like `gitlab.example.com/username/project`) and when mentioning
# them in issues, merge request and comments (like `@username`).
- # If the attribute specified for `username` contains an email address,
+ # If the attribute specified for `username` contains an email address,
# the GitLab username will be the part of the email address before the '@'.
username: ['uid', 'userid', 'sAMAccountName']
email: ['mail', 'email', 'userPrincipalName']
# If no full name could be found at the attribute specified for `name`,
- # the full name is determined using the attributes specified for
+ # the full name is determined using the attributes specified for
# `first_name` and `last_name`.
name: 'cn'
first_name: 'givenName'
@@ -252,28 +287,29 @@ production: &base
# arguments, followed by optional 'args' which can be either a hash or an array.
# Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html
providers:
- # - { name: 'google_oauth2',
- # label: 'Google',
- # app_id: 'YOUR_APP_ID',
- # app_secret: 'YOUR_APP_SECRET',
- # args: { access_type: 'offline', approval_prompt: '' } }
- # - { name: 'twitter',
- # app_id: 'YOUR_APP_ID',
- # app_secret: 'YOUR_APP_SECRET' }
- # - { name: 'github',
- # label: 'GitHub',
- # app_id: 'YOUR_APP_ID',
+ # - { name: 'github',
+ # app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET',
# args: { scope: 'user:email' } }
- # - { name: 'gitlab',
- # label: 'GitLab.com',
- # app_id: 'YOUR_APP_ID',
+ # - { name: 'bitbucket',
+ # app_id: 'YOUR_APP_ID',
+ # app_secret: 'YOUR_APP_SECRET' }
+ # - { name: 'gitlab',
+ # app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET',
# args: { scope: 'api' } }
- # - { name: 'bitbucket',
- # app_id: 'YOUR_APP_ID',
+ # - { name: 'google_oauth2',
+ # app_id: 'YOUR_APP_ID',
+ # app_secret: 'YOUR_APP_SECRET',
+ # args: { access_type: 'offline', approval_prompt: '' } }
+ # - { name: 'facebook',
+ # app_id: 'YOUR_APP_ID',
+ # app_secret: 'YOUR_APP_SECRET' }
+ # - { name: 'twitter',
+ # app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET' }
- # - { name: 'saml',
+ #
+ # - { name: 'saml',
# label: 'Our SAML Provider',
# args: {
# assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
@@ -288,7 +324,9 @@ production: &base
# application_name: 'YOUR_APP_NAME',
# application_password: 'YOUR_APP_PASSWORD' } }
-
+ # Shared file storage settings
+ shared:
+ # path: /mnt/gitlab # Default: shared
#
@@ -296,10 +334,12 @@ production: &base
# ==========================
# GitLab Satellites
+ #
+ # Note for maintainers: keep the satellites.path setting until GitLab 9.0 at
+ # least. This setting is fed to 'rm -rf' in
+ # db/migrate/20151023144219_remove_satellites.rb
satellites:
- # Relative paths are relative to Rails.root (default: tmp/repo_satellites/)
path: /home/git/gitlab-satellites/
- timeout: 30
## Backup settings
backup:
@@ -319,6 +359,8 @@ production: &base
# # Use multipart uploads when file size reaches 100MB, see
# # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html
# multipart_chunk_size: 104857600
+ # # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
+ # # encryption: 'AES256'
## GitLab Shell settings
gitlab_shell:
@@ -387,6 +429,8 @@ test:
<<: *base
gravatar:
enabled: true
+ lfs:
+ enabled: false
gitlab:
host: localhost
port: 80
@@ -422,7 +466,6 @@ test:
user_filter: ''
group_base: 'ou=groups,dc=example,dc=com'
admin_group: ''
- sync_ssh_keys: false
staging:
<<: *base
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 48601b67335..63d8ae17436 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -33,13 +33,15 @@ class Settings < Settingslogic
end
def build_gitlab_shell_ssh_path_prefix
+ user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
+
if gitlab_shell.ssh_port != 22
- "ssh://#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}:#{gitlab_shell.ssh_port}/"
+ "ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
else
if gitlab_shell.ssh_host.include? ':'
- "[#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}]:"
+ "[#{user_host}]:"
else
- "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}:"
+ "#{user_host}:"
end
end
end
@@ -125,6 +127,9 @@ Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link
Settings.omniauth['providers'] ||= []
+Settings['shared'] ||= Settingslogic.new({})
+Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root)
+
Settings['issues_tracker'] ||= {}
#
@@ -159,7 +164,7 @@ Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].
Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil?
Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
-Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil?
+Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil?
Settings.gitlab['default_projects_features'] ||= {}
Settings.gitlab['webhook_timeout'] ||= 10
Settings.gitlab['max_attachment_size'] ||= 10
@@ -168,8 +173,9 @@ Settings.gitlab.default_projects_features['issues'] = true if Settings.g
Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil?
Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil?
Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil?
+Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil?
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
-Settings.gitlab['repository_downloads_path'] = File.absolute_path(Settings.gitlab['repository_downloads_path'] || 'tmp/repositories', Rails.root)
+Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil?
Settings.gitlab['restricted_signup_domains'] ||= []
Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git']
@@ -178,17 +184,36 @@ Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious'
# CI
#
Settings['gitlab_ci'] ||= Settingslogic.new({})
-Settings.gitlab_ci['enabled'] = true if Settings.gitlab_ci['enabled'].nil?
-Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil?
-Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil?
-Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url)
-Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root)
+Settings.gitlab_ci['shared_runners_enabled'] = true if Settings.gitlab_ci['shared_runners_enabled'].nil?
+Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil?
+Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil?
+Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url)
+Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root)
#
# Reply by email
#
Settings['incoming_email'] ||= Settingslogic.new({})
-Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil?
+Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil?
+Settings.incoming_email['port'] = 143 if Settings.incoming_email['port'].nil?
+Settings.incoming_email['ssl'] = false if Settings.incoming_email['ssl'].nil?
+Settings.incoming_email['start_tls'] = false if Settings.incoming_email['start_tls'].nil?
+Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil?
+
+#
+# Build Artifacts
+#
+Settings['artifacts'] ||= Settingslogic.new({})
+Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil?
+Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root)
+Settings.artifacts['max_size'] ||= 100 # in megabytes
+
+#
+# Git LFS
+#
+Settings['lfs'] ||= Settingslogic.new({})
+Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil?
+Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root)
#
# Gravatar
@@ -229,6 +254,7 @@ if Settings.backup['upload']['connection']
Settings.backup['upload']['connection'] = Hash[Settings.backup['upload']['connection'].map { |k, v| [k.to_sym, v] }]
end
Settings.backup['upload']['multipart_chunk_size'] ||= 104857600
+Settings.backup['upload']['encryption'] ||= nil
#
# Git
@@ -238,9 +264,12 @@ Settings.git['max_size'] ||= 20971520 # 20.megabytes
Settings.git['bin_path'] ||= '/usr/bin/git'
Settings.git['timeout'] ||= 10
+# Important: keep the satellites.path setting until GitLab 9.0 at
+# least. This setting is fed to 'rm -rf' in
+# db/migrate/20151023144219_remove_satellites.rb
Settings['satellites'] ||= Settingslogic.new({})
Settings.satellites['path'] = File.expand_path(Settings.satellites['path'] || "tmp/repo_satellites/", Rails.root)
-Settings.satellites['timeout'] ||= 30
+
#
# Extra customization
@@ -266,3 +295,12 @@ if Rails.env.test?
Settings.gitlab['default_can_create_group'] = true
Settings.gitlab['default_can_create_team'] = false
end
+
+# Force a refresh of application settings at startup
+begin
+ ApplicationSetting.expire
+ Ci::ApplicationSetting.expire
+rescue
+ # Gracefully handle when Redis is not available. For example,
+ # omnibus may fail here during assets:precompile.
+end
diff --git a/config/initializers/2_app.rb b/config/initializers/2_app.rb
index 688cdf5f4b0..35b150c9929 100644
--- a/config/initializers/2_app.rb
+++ b/config/initializers/2_app.rb
@@ -1,8 +1,8 @@
module Gitlab
- VERSION = File.read(Rails.root.join("VERSION")).strip
- REVISION = Gitlab::Popen.popen(%W(git log --pretty=format:%h -n 1)).first.chomp
-
def self.config
Settings
end
+
+ VERSION = File.read(Rails.root.join("VERSION")).strip
+ REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp
end
diff --git a/config/initializers/active_record_query_trace.rb b/config/initializers/active_record_query_trace.rb
new file mode 100644
index 00000000000..4b3c2803b3b
--- /dev/null
+++ b/config/initializers/active_record_query_trace.rb
@@ -0,0 +1,5 @@
+if ENV['ENABLE_QUERY_TRACE']
+ require 'active_record_query_trace'
+
+ ActiveRecordQueryTrace.enabled = 'true'
+end
diff --git a/config/initializers/bullet.rb b/config/initializers/bullet.rb
new file mode 100644
index 00000000000..95e82966c7a
--- /dev/null
+++ b/config/initializers/bullet.rb
@@ -0,0 +1,6 @@
+if ENV['ENABLE_BULLET']
+ require 'bullet'
+
+ Bullet.enable = true
+ Bullet.console = true
+end
diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb
index 43adac8b2c6..54516e3f23d 100644
--- a/config/initializers/cookies_serializer.rb
+++ b/config/initializers/cookies_serializer.rb
@@ -1,3 +1,3 @@
# Be sure to restart your server when you modify this file.
-Gitlab::Application.config.action_dispatch.cookies_serializer = :hybrid
+Rails.application.config.action_dispatch.cookies_serializer = :hybrid
diff --git a/config/initializers/default_url_options.rb b/config/initializers/default_url_options.rb
index f9f88f95db9..8fd27b1d88e 100644
--- a/config/initializers/default_url_options.rb
+++ b/config/initializers/default_url_options.rb
@@ -8,4 +8,4 @@ unless Gitlab.config.gitlab_on_standard_port?
default_url_options[:port] = Gitlab.config.gitlab.port
end
-Gitlab::Application.routes.default_url_options = default_url_options
+Rails.application.routes.default_url_options = default_url_options
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 2ce24592f8b..29506970af2 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -148,6 +148,10 @@ Devise.setup do |config|
# When someone else invites you to GitLab this time is also used so it should be pretty long.
config.reset_password_within = 2.days
+ # When set to false, does not sign a user in automatically after their password is
+ # reset. Defaults to true, so a user is signed in automatically after a reset.
+ config.sign_in_after_reset_password = false
+
# ==> Configuration for :encryptable
# Allow you to use another encryption algorithm besides bcrypt (default). You can use
# :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1,
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 5d46ece1e1b..9e8b0131f8f 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -8,24 +8,3 @@
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
-
-# Mark "commits" as uncountable.
-#
-# Without this change, the routes
-#
-# resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/}
-# resources :commits, only: [:show], constraints: {id: /.+/}
-#
-# would generate identical route helper methods (`project_commit_path`), resulting
-# in one of them not getting a helper method at all.
-#
-# After this change, the helper methods are:
-#
-# project_commit_path(@project, @project.commit)
-# # => "/gitlabhq/commit/bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a
-#
-# project_commits_path(@project, 'stable/README.md')
-# # => "/gitlabhq/commits/stable/README.md"
-ActiveSupport::Inflector.inflections do |inflect|
- inflect.uncountable %w(commits)
-end
diff --git a/config/initializers/rack_attack.rb.example b/config/initializers/rack_attack.rb.example
index 2155ea14562..b1bbcca1d61 100644
--- a/config/initializers/rack_attack.rb.example
+++ b/config/initializers/rack_attack.rb.example
@@ -4,13 +4,13 @@
# If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
paths_to_be_protected = [
- "#{Gitlab::Application.config.relative_url_root}/users/password",
- "#{Gitlab::Application.config.relative_url_root}/users/sign_in",
- "#{Gitlab::Application.config.relative_url_root}/api/#{API::API.version}/session.json",
- "#{Gitlab::Application.config.relative_url_root}/api/#{API::API.version}/session",
- "#{Gitlab::Application.config.relative_url_root}/users",
- "#{Gitlab::Application.config.relative_url_root}/users/confirmation",
- "#{Gitlab::Application.config.relative_url_root}/unsubscribes/"
+ "#{Rails.application.config.relative_url_root}/users/password",
+ "#{Rails.application.config.relative_url_root}/users/sign_in",
+ "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session.json",
+ "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session",
+ "#{Rails.application.config.relative_url_root}/users",
+ "#{Rails.application.config.relative_url_root}/users/confirmation",
+ "#{Rails.application.config.relative_url_root}/unsubscribes/"
]
diff --git a/config/initializers/rack_lineprof.rb b/config/initializers/rack_lineprof.rb
new file mode 100644
index 00000000000..22e77a32c61
--- /dev/null
+++ b/config/initializers/rack_lineprof.rb
@@ -0,0 +1,31 @@
+# The default colors of rack-lineprof can be very hard to look at in terminals
+# with darker backgrounds. This patch tweaks the colors a bit so the output is
+# actually readable.
+if Rails.env.development? and RUBY_ENGINE == 'ruby' and ENV['ENABLE_LINEPROF']
+ Rails.application.config.middleware.use(Rack::Lineprof)
+
+ module Rack
+ class Lineprof
+ class Sample < Rack::Lineprof::Sample.superclass
+ def format(*)
+ formatted = if level == CONTEXT
+ sprintf " | % 3i %s", line, code
+ else
+ sprintf "% 8.1fms %5i | % 3i %s", ms, calls, line, code
+ end
+
+ case level
+ when CRITICAL
+ color.red formatted
+ when WARNING
+ color.yellow formatted
+ when NOMINAL
+ color.white formatted
+ else # CONTEXT
+ formatted
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/config/initializers/rack_profiler.rb b/config/initializers/rack_profiler.rb
deleted file mode 100644
index 7710eeac453..00000000000
--- a/config/initializers/rack_profiler.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-if Rails.env.development?
- require 'rack-mini-profiler'
-
- # initialization is skipped so trigger it
- Rack::MiniProfilerRails.initialize!(Gitlab::Application)
-
- Rack::MiniProfiler.config.position = 'right'
- Rack::MiniProfiler.config.start_hidden = false
- Rack::MiniProfiler.config.skip_paths << '/teaspoon'
-end
diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
index 1b518c3becf..dae3a4a9a93 100644
--- a/config/initializers/secret_token.rb
+++ b/config/initializers/secret_token.rb
@@ -22,15 +22,15 @@ def find_secure_token
end
end
-Gitlab::Application.config.secret_token = find_secure_token
-Gitlab::Application.config.secret_key_base = find_secure_token
+Rails.application.config.secret_token = find_secure_token
+Rails.application.config.secret_key_base = find_secure_token
# CI
def generate_new_secure_token
SecureRandom.hex(64)
end
-if Gitlab::Application.secrets.db_key_base.blank?
+if Rails.application.secrets.db_key_base.blank?
warn "Missing `db_key_base` for '#{Rails.env}' environment. The secrets will be generated and stored in `config/secrets.yml`"
all_secrets = YAML.load_file('config/secrets.yml') if File.exist?('config/secrets.yml')
@@ -46,5 +46,5 @@ if Gitlab::Application.secrets.db_key_base.blank?
file.write(YAML.dump(all_secrets))
end
- Gitlab::Application.secrets.db_key_base = env_secrets['db_key_base']
+ Rails.application.secrets.db_key_base = env_secrets['db_key_base']
end
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 04ed9e90df5..d5208b8c93e 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -3,18 +3,21 @@
require 'gitlab/current_settings'
include Gitlab::CurrentSettings
-# allow it to fail: it may to do so when create_from_defaults is executed before migrations are actually done
+# allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done
begin
- Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay
+ Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay || 10080
rescue
+ Settings.gitlab['session_expire_delay'] ||= 10080
end
-Gitlab::Application.config.session_store(
- :redis_store, # Using the cookie_store would enable session replay attacks.
- servers: Gitlab::Application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store
- key: '_gitlab_session',
- secure: Gitlab.config.gitlab.https,
- httponly: true,
- expire_after: Settings.gitlab['session_expire_delay'] * 60,
- path: (Gitlab::Application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root
-)
+unless Rails.env.test?
+ Gitlab::Application.config.session_store(
+ :redis_store, # Using the cookie_store would enable session replay attacks.
+ servers: Rails.application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store
+ key: '_gitlab_session',
+ secure: Gitlab.config.gitlab.https,
+ httponly: true,
+ expire_after: Settings.gitlab['session_expire_delay'] * 60,
+ path: (Rails.application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root
+ )
+end
diff --git a/config/initializers/sherlock.rb b/config/initializers/sherlock.rb
new file mode 100644
index 00000000000..8f2ababb712
--- /dev/null
+++ b/config/initializers/sherlock.rb
@@ -0,0 +1,5 @@
+if Gitlab::Sherlock.enabled?
+ Rails.application.configure do |config|
+ config.middleware.use(Gitlab::Sherlock::Middleware)
+ end
+end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index e856499732e..6e5701e33da 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -17,6 +17,12 @@ Sidekiq.configure_server do |config|
chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS']
chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS']
end
+
+ # Sidekiq-cron: load recurring jobs from schedule.yml
+ schedule_file = 'config/schedule.yml'
+ if File.exists?(schedule_file)
+ Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file)
+ end
end
Sidekiq.configure_client do |config|
diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample
index 25ec247a095..ec182502d4e 100644
--- a/config/initializers/smtp_settings.rb.sample
+++ b/config/initializers/smtp_settings.rb.sample
@@ -8,7 +8,7 @@
# If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
if Rails.env.production?
- Gitlab::Application.config.action_mailer.delivery_method = :smtp
+ Rails.application.config.action_mailer.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
address: "email.server.com",
diff --git a/config/initializers/state_machine_patch.rb b/config/initializers/state_machine_patch.rb
deleted file mode 100644
index 72d010fa5de..00000000000
--- a/config/initializers/state_machine_patch.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# This is a patch to address the issue in https://github.com/pluginaweek/state_machine/issues/251
-# where gem 'state_machine' was not working for Rails 4.1
-module StateMachine
- module Integrations
- module ActiveModel
- public :around_validation
- end
- end
-end
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index e6d5600edb7..d6dbf8b9fbf 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -1,6 +1,6 @@
-app = Gitlab::Application
+app = Rails.application
-if app.config.serve_static_assets
+if app.config.serve_static_files
# The `ActionDispatch::Static` middleware intercepts requests for static files
# by checking if they exist in the `/public` directory.
# We're replacing it with our `Gitlab::Middleware::Static` that does the same,
diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml
index d8bf0878a3d..22070e37f07 100644
--- a/config/locales/devise.en.yml
+++ b/config/locales/devise.en.yml
@@ -1,61 +1,63 @@
-# Additional translations at http://github.com/plataformatec/devise/wiki/I18n
+# Additional translations at https://github.com/plataformatec/devise/wiki/I18n
en:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ invalid: "Invalid %{authentication_keys} or password."
+ locked: "Your account is locked."
+ last_attempt: "You have one more attempt before your account is locked."
+ not_found_in_database: "Invalid %{authentication_keys} or password."
+ timeout: "Your session expired. Please sign in again to continue."
+ unauthenticated: "You need to sign in or sign up before continuing."
+ unconfirmed: "You have to confirm your email address before continuing."
+ mailer:
+ confirmation_instructions:
+ subject: "Confirmation instructions"
+ reset_password_instructions:
+ subject: "Reset password instructions"
+ unlock_instructions:
+ subject: "Unlock instructions"
+ password_change:
+ subject: "Password Changed"
+ omniauth_callbacks:
+ failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
+ success: "Successfully authenticated from %{kind} account."
+ passwords:
+ no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
+ recently_reset: "Instructions about how to reset your password have already been sent recently. Please wait a few minutes to try again."
+ send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
+ updated: "Your password has been changed successfully. You are now signed in."
+ updated_not_active: "Your password has been changed successfully."
+ registrations:
+ destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
+ signed_up: "Welcome! You have signed up successfully."
+ signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
+ signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
+ signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
+ update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address."
+ updated: "Your account has been updated successfully."
+ sessions:
+ signed_in: "Signed in successfully."
+ signed_out: "Signed out successfully."
+ already_signed_out: "Signed out successfully."
+ unlocks:
+ send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
+ send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
+ unlocked: "Your account has been unlocked successfully. Please sign in to continue."
errors:
messages:
+ already_confirmed: "was already confirmed, please try signing in"
+ confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
expired: "has expired, please request a new one"
not_found: "not found"
- already_confirmed: "was already confirmed, please try signing in"
not_locked: "was not locked"
not_saved:
one: "1 error prohibited this %{resource} from being saved:"
other: "%{count} errors prohibited this %{resource} from being saved:"
-
- devise:
- failure:
- already_authenticated: 'You are already signed in.'
- unauthenticated: 'You need to sign in before continuing.'
- unconfirmed: 'You have to confirm your account before continuing.'
- locked: 'Your account is locked.'
- not_found_in_database: 'Invalid email or password.'
- invalid: 'Invalid email or password.'
- invalid_token: 'Invalid authentication token.'
- timeout: 'Your session expired, please sign in again to continue.'
- inactive: 'Your account was not activated yet.'
- sessions:
- signed_in: ''
- signed_out: ''
- users_sessions:
- user:
- signed_in: 'Signed in successfully.'
- passwords:
- send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.'
- updated: 'Your password was changed successfully. You are now signed in.'
- updated_not_active: 'Your password was changed successfully.'
- send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
- no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
- confirmations:
- send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.'
- send_paranoid_instructions: 'If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes.'
- confirmed: 'Your account was successfully confirmed. You are now signed in.'
- registrations:
- signed_up: 'Welcome! You have signed up successfully.'
- updated: 'You updated your account successfully.'
- destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.'
- signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.'
- signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.'
- signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.'
- unlocks:
- send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.'
- unlocked: 'Your account was successfully unlocked. You are now signed in.'
- send_paranoid_instructions: 'If your account exists, you will receive an email with instructions about how to unlock it in a few minutes.'
- omniauth_callbacks:
- success: 'Successfully authorized from %{kind} account.'
- failure: 'Could not authorize you from %{kind} because "%{reason}".'
- mailer:
- confirmation_instructions:
- subject: 'Confirmation instructions'
- reset_password_instructions:
- subject: 'Reset password instructions'
- unlock_instructions:
- subject: 'Unlock Instructions'
diff --git a/config/locales/sherlock.en.yml b/config/locales/sherlock.en.yml
new file mode 100644
index 00000000000..f24b825f585
--- /dev/null
+++ b/config/locales/sherlock.en.yml
@@ -0,0 +1,38 @@
+en:
+ sherlock:
+ title: Sherlock
+ delete_all_transactions: Delete All Transactions
+ introduction: >
+ Below is a list of all transactions recorded by Sherlock. Requests to
+ Sherlock's own routes are ignored.
+ no_transactions: No transactions to show
+ no_queries: No queries to show
+ no_file_samples: No file samples to show
+ all_transactions: All Transactions
+ transaction: Transaction
+ query: Query
+ file_sample: File Sample
+ type: Type
+ path: Path
+ time: Time
+ queries: Queries
+ finished_at: Finished at
+ ago: ago
+ view: View
+ seconds: seconds
+ milliseconds: ms
+ general: General
+ id: ID
+ time_inclusive: Time (inclusive)
+ backtrace: Backtrace
+ application_backtrace: Application Backtrace
+ full_backtrace: Full Backtrace
+ origin: Origin
+ line: line
+ line_capitalized: Line
+ copy_to_clipboard: Copy to clipboard
+ query_plan: Query Plan
+ events: Events
+ percent: '%'
+ count: Count
+ query_time: Query Time
diff --git a/config/mail_room.yml b/config/mail_room.yml
new file mode 100644
index 00000000000..42f6f74c465
--- /dev/null
+++ b/config/mail_room.yml
@@ -0,0 +1,39 @@
+:mailboxes:
+<%
+require_relative 'config/environment.rb'
+
+if Gitlab::IncomingEmail.enabled?
+ config = Gitlab::IncomingEmail.config
+
+ redis_config_file = "config/resque.yml"
+ redis_url =
+ if File.exists?(redis_config_file)
+ YAML.load_file(redis_config_file)[Rails.env]
+ else
+ "redis://localhost:6379"
+ end
+ %>
+ -
+ :host: <%= config.host.to_json %>
+ :port: <%= config.port.to_json %>
+ :ssl: <%= config.ssl.to_json %>
+ :start_tls: <%= config.start_tls.to_json %>
+ :email: <%= config.user.to_json %>
+ :password: <%= config.password.to_json %>
+
+ :name: <%= config.mailbox.to_json %>
+
+ :delete_after_delivery: true
+
+ :delivery_method: sidekiq
+ :delivery_options:
+ :redis_url: <%= redis_url.to_json %>
+ :namespace: resque:gitlab
+ :queue: incoming_email
+ :worker: EmailReceiverWorker
+
+ :arbitration_method: redis
+ :arbitration_options:
+ :redis_url: <%= redis_url.to_json %>
+ :namespace: mail_room:gitlab
+<% end %>
diff --git a/config/mail_room.yml.example b/config/mail_room.yml.example
deleted file mode 100644
index 82e1a42058e..00000000000
--- a/config/mail_room.yml.example
+++ /dev/null
@@ -1,29 +0,0 @@
-:mailboxes:
- -
- # # IMAP server host
- # :host: "imap.gmail.com"
- # # IMAP server port
- # :port: 993
- # # Whether the IMAP server uses SSL
- # :ssl: true
- # # Whether the IMAP server uses StartTLS
- # :start_tls: false
- # # Email account username. Usually the full email address.
- # :email: "replies@gitlab.example.com"
- # # Email account password
- # :password: "password"
- # # The name of the mailbox where incoming mail will end up. Usually "inbox".
- # :name: "inbox"
- # # Always "sidekiq".
- # :delivery_method: sidekiq
- # # Always true.
- # :delete_after_delivery: true
- # :delivery_options:
- # # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml.
- # :redis_url: redis://localhost:6379
- # # Always "resque:gitlab".
- # :namespace: resque:gitlab
- # # Always "incoming_email".
- # :queue: incoming_email
- # # Always "EmailReceiverWorker"
- # :worker: EmailReceiverWorker
diff --git a/config/routes.rb b/config/routes.rb
index 4a07c449b4e..3b151891a6b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,7 +1,21 @@
require 'sidekiq/web'
+require 'sidekiq/cron/web'
require 'api/api'
-Gitlab::Application.routes.draw do
+Rails.application.routes.draw do
+ if Gitlab::Sherlock.enabled?
+ namespace :sherlock do
+ resources :transactions, only: [:index, :show] do
+ resources :queries, only: [:show]
+ resources :file_samples, only: [:show]
+
+ collection do
+ delete :destroy_all
+ end
+ end
+ end
+ end
+
namespace :ci do
# CI API
Ci::API::API.logger Rails.logger
@@ -19,53 +33,9 @@ Gitlab::Application.routes.draw do
get :status, to: 'projects#badge'
get :integration
post :toggle_shared_runners
- get :dumped_yaml
- end
-
- resources :services, only: [:index, :edit, :update] do
- member do
- get :test
- end
- end
-
- resource :charts, only: [:show]
-
- resources :refs, constraints: { ref_id: /.*/ }, only: [] do
- resources :commits, only: [:show] do
- member do
- get :status
- get :cancel
- end
- end
- end
-
- resources :builds, only: [:show] do
- member do
- get :cancel
- get :status
- post :retry
- end
- end
-
- resources :web_hooks, only: [:index, :create, :destroy] do
- member do
- get :test
- end
- end
-
- resources :triggers, only: [:index, :create, :destroy]
-
- resources :runners, only: [:index, :edit, :update, :destroy, :show] do
- member do
- get :resume
- get :pause
- end
end
resources :runner_projects, only: [:create, :destroy]
-
- resources :events, only: [:index]
- resource :variables, only: [:show, :update]
end
resource :user_sessions do
@@ -124,7 +94,7 @@ Gitlab::Application.routes.draw do
end
# Enable Grack support
- mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post]
+ mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put]
# Help
get 'help' => 'help#index'
@@ -253,6 +223,8 @@ Gitlab::Application.routes.draw do
resources :keys, only: [:show, :destroy]
resources :identities, only: [:index, :edit, :update, :destroy]
+ delete 'stop_impersonation' => 'impersonation#destroy', on: :collection
+
member do
get :projects
get :keys
@@ -262,6 +234,7 @@ Gitlab::Application.routes.draw do
put :unblock
put :unlock
put :confirm
+ post 'impersonate' => 'impersonation#create'
patch :disable_two_factor
delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
end
@@ -396,7 +369,7 @@ Gitlab::Application.routes.draw do
end
resource :avatar, only: [:destroy]
- resources :milestones, only: [:index, :show, :update]
+ resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
end
end
@@ -418,6 +391,7 @@ Gitlab::Application.routes.draw do
[:new, :create, :index], path: "/") do
member do
put :transfer
+ delete :remove_fork
post :archive
post :unarchive
post :toggle_star
@@ -483,6 +457,15 @@ Gitlab::Application.routes.draw do
end
scope do
+ post(
+ '/create_dir/*id',
+ to: 'tree#create_dir',
+ constraints: { id: /.+/ },
+ as: 'create_dir'
+ )
+ end
+
+ scope do
get(
'/blame/*id',
to: 'blame#show',
@@ -502,7 +485,12 @@ Gitlab::Application.routes.draw do
resource :avatar, only: [:show, :destroy]
resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do
- get :branches, on: :member
+ member do
+ get :branches
+ get :builds
+ post :cancel_builds
+ post :retry_builds
+ end
end
resources :compare, only: [:index, :create]
@@ -570,8 +558,10 @@ Gitlab::Application.routes.draw do
member do
# tree viewer logs
get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
+ # Directories with leading dots erroneously get rejected if git
+ # ref regex used in constraints. Regex verification now done in controller.
get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
- id: Gitlab::Regex.git_reference_regex,
+ id: /.*/,
path: /.*/
}
end
@@ -595,8 +585,38 @@ Gitlab::Application.routes.draw do
end
resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
- resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+ resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
+ resource :release, only: [:edit, :update]
+ end
+
resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+ resource :variables, only: [:show, :update]
+ resources :triggers, only: [:index, :create, :destroy]
+ resource :ci_settings, only: [:edit, :update, :destroy]
+ resources :ci_web_hooks, only: [:index, :create, :destroy] do
+ member do
+ get :test
+ end
+ end
+
+ resources :ci_services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
+ member do
+ get :test
+ end
+ end
+
+ resources :builds, only: [:index, :show] do
+ collection do
+ post :cancel_all
+ end
+
+ member do
+ get :status
+ post :cancel
+ get :download
+ post :retry
+ end
+ end
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
member do
@@ -645,6 +665,10 @@ Gitlab::Application.routes.draw do
member do
delete :delete_attachment
end
+
+ collection do
+ post :award_toggle
+ end
end
resources :uploads, only: [:create] do
@@ -652,8 +676,14 @@ Gitlab::Application.routes.draw do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
end
end
- end
+ resources :runners, only: [:index, :edit, :update, :destroy, :show] do
+ member do
+ get :resume
+ get :pause
+ end
+ end
+ end
end
end
diff --git a/config/schedule.yml b/config/schedule.yml
new file mode 100644
index 00000000000..993a95fef56
--- /dev/null
+++ b/config/schedule.yml
@@ -0,0 +1,10 @@
+# Here is a list of jobs that are scheduled to run periodically.
+# We use a UNIX cron notation to specify execution schedule.
+#
+# Please read here for more information:
+# https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job
+
+stuck_ci_builds_worker:
+ cron: "0 0 * * *"
+ class: "StuckCiBuildsWorker"
+ queue: "default"
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index 8f71198e47f..b4639999967 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -73,8 +73,13 @@ Sidekiq::Testing.inline! do
}
project = Projects::CreateService.new(User.first, params).execute
+ # Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
+ # hook won't run until after the fixture is loaded. That is too late
+ # since the Sidekiq::Testing block has already exited. Force clearing
+ # the `after_commit` queue to ensure the job is run now.
+ project.send(:_run_after_commit_queue)
- if project.valid?
+ if project.valid? && project.valid_repo?
print '.'
else
puts project.errors.full_messages
diff --git a/db/fixtures/development/05_users.rb b/db/fixtures/development/05_users.rb
index 378354efd5a..03da29c4c68 100644
--- a/db/fixtures/development/05_users.rb
+++ b/db/fixtures/development/05_users.rb
@@ -1,5 +1,5 @@
Gitlab::Seeder.quiet do
- (2..20).each do |i|
+ 20.times do |i|
begin
User.create!(
username: FFaker::Internet.user_name,
@@ -15,7 +15,7 @@ Gitlab::Seeder.quiet do
end
end
- (1..5).each do |i|
+ 5.times do |i|
begin
User.create!(
username: "user#{i}",
diff --git a/db/fixtures/development/07_milestones.rb b/db/fixtures/development/07_milestones.rb
index a43116829d9..e028ac82ba3 100644
--- a/db/fixtures/development/07_milestones.rb
+++ b/db/fixtures/development/07_milestones.rb
@@ -1,6 +1,6 @@
Gitlab::Seeder.quiet do
Project.all.each do |project|
- (1..5).each do |i|
+ 5.times do |i|
milestone_params = {
title: "v#{i}.0",
description: FFaker::Lorem.sentence,
diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb
index c636e96381c..4fa572fca9b 100644
--- a/db/fixtures/development/09_issues.rb
+++ b/db/fixtures/development/09_issues.rb
@@ -1,6 +1,6 @@
Gitlab::Seeder.quiet do
Project.all.each do |project|
- (1..10).each do |i|
+ 10.times do
issue_params = {
title: FFaker::Lorem.sentence(6),
description: FFaker::Lorem.sentence,
diff --git a/db/fixtures/development/12_snippets.rb b/db/fixtures/development/12_snippets.rb
index 3bd4b442ade..74898544a69 100644
--- a/db/fixtures/development/12_snippets.rb
+++ b/db/fixtures/development/12_snippets.rb
@@ -22,7 +22,7 @@ class Member < ActiveRecord::Base
end
eos
- (1..50).each do |i|
+ 50.times do |i|
user = User.all.sample
PersonalSnippet.seed(:id, [{
diff --git a/db/migrate/20150924125150_add_project_id_to_ci_commit.rb b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb
new file mode 100644
index 00000000000..1a761fe0f86
--- /dev/null
+++ b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb
@@ -0,0 +1,5 @@
+class AddProjectIdToCiCommit < ActiveRecord::Migration
+ def up
+ add_column :ci_commits, :gl_project_id, :integer
+ end
+end
diff --git a/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb
new file mode 100644
index 00000000000..2be57b6062e
--- /dev/null
+++ b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb
@@ -0,0 +1,6 @@
+class MigrateProjectIdForCiCommits < ActiveRecord::Migration
+ def up
+ subquery = 'SELECT gitlab_id FROM ci_projects WHERE ci_projects.id = ci_commits.project_id'
+ execute("UPDATE ci_commits SET gl_project_id=(#{subquery}) WHERE gl_project_id IS NULL")
+ end
+end
diff --git a/db/migrate/20150930001110_merge_request_error_field.rb b/db/migrate/20150930001110_merge_request_error_field.rb
new file mode 100644
index 00000000000..c2ee498ef3f
--- /dev/null
+++ b/db/migrate/20150930001110_merge_request_error_field.rb
@@ -0,0 +1,5 @@
+class MergeRequestErrorField < ActiveRecord::Migration
+ def up
+ add_column :merge_requests, :merge_error, :string
+ end
+end
diff --git a/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb
new file mode 100644
index 00000000000..8d47dac6441
--- /dev/null
+++ b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb
@@ -0,0 +1,9 @@
+class AddNullToNameForCiProjects < ActiveRecord::Migration
+ def up
+ change_column_null :ci_projects, :name, true
+ end
+
+ def down
+ change_column_null :ci_projects, :name, false
+ end
+end
diff --git a/db/migrate/20151002112914_add_stage_idx_to_builds.rb b/db/migrate/20151002112914_add_stage_idx_to_builds.rb
new file mode 100644
index 00000000000..68a745ffef4
--- /dev/null
+++ b/db/migrate/20151002112914_add_stage_idx_to_builds.rb
@@ -0,0 +1,5 @@
+class AddStageIdxToBuilds < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :stage_idx, :integer
+ end
+end
diff --git a/db/migrate/20151002121400_add_index_for_builds.rb b/db/migrate/20151002121400_add_index_for_builds.rb
new file mode 100644
index 00000000000..4ffc1363910
--- /dev/null
+++ b/db/migrate/20151002121400_add_index_for_builds.rb
@@ -0,0 +1,5 @@
+class AddIndexForBuilds < ActiveRecord::Migration
+ def up
+ add_index :ci_builds, [:commit_id, :stage_idx, :created_at]
+ end
+end
diff --git a/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb b/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb
new file mode 100644
index 00000000000..e3d2ac1cea5
--- /dev/null
+++ b/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb
@@ -0,0 +1,6 @@
+class AddRefAndTagToBuilds < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :tag, :boolean
+ add_column :ci_builds, :ref, :string
+ end
+end
diff --git a/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb b/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb
new file mode 100644
index 00000000000..01d7b3f6773
--- /dev/null
+++ b/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb
@@ -0,0 +1,6 @@
+class MigrateRefAndTagToBuild < ActiveRecord::Migration
+ def change
+ execute('UPDATE ci_builds SET ref=(SELECT ref FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE ref IS NULL')
+ execute('UPDATE ci_builds SET tag=(SELECT tag FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE tag IS NULL')
+ end
+end
diff --git a/db/migrate/20151005075649_add_user_id_to_build.rb b/db/migrate/20151005075649_add_user_id_to_build.rb
new file mode 100644
index 00000000000..0f4b92b8b79
--- /dev/null
+++ b/db/migrate/20151005075649_add_user_id_to_build.rb
@@ -0,0 +1,5 @@
+class AddUserIdToBuild < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :user_id, :integer
+ end
+end
diff --git a/db/migrate/20151005150751_add_layout_option_for_users.rb b/db/migrate/20151005150751_add_layout_option_for_users.rb
new file mode 100644
index 00000000000..ead9b1f8977
--- /dev/null
+++ b/db/migrate/20151005150751_add_layout_option_for_users.rb
@@ -0,0 +1,5 @@
+class AddLayoutOptionForUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :layout, :integer, default: 0
+ end
+end \ No newline at end of file
diff --git a/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb b/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb
new file mode 100644
index 00000000000..be6aa810bb5
--- /dev/null
+++ b/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb
@@ -0,0 +1,5 @@
+class RemoveCiEnabledFromApplicationSettings < ActiveRecord::Migration
+ def change
+ remove_column :application_settings, :ci_enabled, :boolean, null: false, default: true
+ end
+end
diff --git a/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb b/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb
new file mode 100644
index 00000000000..7f6cd6d5a78
--- /dev/null
+++ b/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb
@@ -0,0 +1,17 @@
+class NamespacesProjectsPathLowerIndexes < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ execute 'CREATE INDEX CONCURRENTLY index_on_namespaces_lower_path ON namespaces (LOWER(path));'
+ execute 'CREATE INDEX CONCURRENTLY index_on_projects_lower_path ON projects (LOWER(path));'
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ remove_index :namespaces, name: :index_on_namespaces_lower_path
+ remove_index :projects, name: :index_on_projects_lower_path
+ end
+end
diff --git a/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb b/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb
new file mode 100644
index 00000000000..2f2dc776785
--- /dev/null
+++ b/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb
@@ -0,0 +1,17 @@
+class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_username ON users (LOWER(username));'
+ execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_email ON users (LOWER(email));'
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ remove_index :users, :index_on_users_lower_username
+ remove_index :users, :index_on_users_lower_email
+ end
+end
diff --git a/db/migrate/20151008123042_add_type_and_description_to_builds.rb b/db/migrate/20151008123042_add_type_and_description_to_builds.rb
new file mode 100644
index 00000000000..c72b1c611c6
--- /dev/null
+++ b/db/migrate/20151008123042_add_type_and_description_to_builds.rb
@@ -0,0 +1,9 @@
+class AddTypeAndDescriptionToBuilds < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :type, :string
+ add_column :ci_builds, :target_url, :string
+ add_column :ci_builds, :description, :string
+ add_index :ci_builds, [:commit_id, :type, :ref]
+ add_index :ci_builds, [:commit_id, :type, :name, :ref]
+ end
+end
diff --git a/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb
new file mode 100644
index 00000000000..f5c44babd84
--- /dev/null
+++ b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb
@@ -0,0 +1,5 @@
+class MigrateNameToDescriptionForBuilds < ActiveRecord::Migration
+ def change
+ execute("UPDATE ci_builds SET type='Ci::Build' WHERE type IS NULL")
+ end
+end
diff --git a/db/migrate/20151008143519_add_admin_notification_email_setting.rb b/db/migrate/20151008143519_add_admin_notification_email_setting.rb
new file mode 100644
index 00000000000..0bb581efe2c
--- /dev/null
+++ b/db/migrate/20151008143519_add_admin_notification_email_setting.rb
@@ -0,0 +1,5 @@
+class AddAdminNotificationEmailSetting < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :admin_notification_email, :string
+ end
+end
diff --git a/db/migrate/20151013092124_add_artifacts_file_to_builds.rb b/db/migrate/20151013092124_add_artifacts_file_to_builds.rb
new file mode 100644
index 00000000000..5a299f7b26d
--- /dev/null
+++ b/db/migrate/20151013092124_add_artifacts_file_to_builds.rb
@@ -0,0 +1,5 @@
+class AddArtifactsFileToBuilds < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :artifacts_file, :text
+ end
+end
diff --git a/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb b/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb
new file mode 100644
index 00000000000..52a47aa9c54
--- /dev/null
+++ b/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb
@@ -0,0 +1,5 @@
+class AddCiProjectsGlProjectIdIndex < ActiveRecord::Migration
+ def change
+ add_index :ci_commits, :gl_project_id
+ end
+end
diff --git a/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb b/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb
new file mode 100644
index 00000000000..7f1af1c7583
--- /dev/null
+++ b/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb
@@ -0,0 +1,9 @@
+class AddCiBuildsAndProjectsIndexes < ActiveRecord::Migration
+ def change
+ add_index :ci_projects, :gitlab_id
+ add_index :ci_projects, :shared_runners_enabled
+
+ add_index :ci_builds, :type
+ add_index :ci_builds, :status
+ end
+end
diff --git a/db/migrate/20151016195706_add_notes_line_code_index.rb b/db/migrate/20151016195706_add_notes_line_code_index.rb
new file mode 100644
index 00000000000..aeeb1a759fa
--- /dev/null
+++ b/db/migrate/20151016195706_add_notes_line_code_index.rb
@@ -0,0 +1,5 @@
+class AddNotesLineCodeIndex < ActiveRecord::Migration
+ def change
+ add_index :notes, :line_code
+ end
+end
diff --git a/db/migrate/20151019111551_fix_build_tags.rb b/db/migrate/20151019111551_fix_build_tags.rb
new file mode 100644
index 00000000000..299a24b0a7c
--- /dev/null
+++ b/db/migrate/20151019111551_fix_build_tags.rb
@@ -0,0 +1,9 @@
+class FixBuildTags < ActiveRecord::Migration
+ def up
+ execute("UPDATE taggings SET taggable_type='CommitStatus' WHERE taggable_type='Ci::Build'")
+ end
+
+ def down
+ execute("UPDATE taggings SET taggable_type='Ci::Build' WHERE taggable_type='CommitStatus'")
+ end
+end
diff --git a/db/migrate/20151019111703_fail_build_without_names.rb b/db/migrate/20151019111703_fail_build_without_names.rb
new file mode 100644
index 00000000000..dcdb5d1b25d
--- /dev/null
+++ b/db/migrate/20151019111703_fail_build_without_names.rb
@@ -0,0 +1,8 @@
+class FailBuildWithoutNames < ActiveRecord::Migration
+ def up
+ execute("UPDATE ci_builds SET status='failed' WHERE name IS NULL AND status='pending'")
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20151020145526_add_services_template_index.rb b/db/migrate/20151020145526_add_services_template_index.rb
new file mode 100644
index 00000000000..1b04f313565
--- /dev/null
+++ b/db/migrate/20151020145526_add_services_template_index.rb
@@ -0,0 +1,5 @@
+class AddServicesTemplateIndex < ActiveRecord::Migration
+ def change
+ add_index :services, :template
+ end
+end
diff --git a/db/migrate/20151020173516_ci_limits_to_mysql.rb b/db/migrate/20151020173516_ci_limits_to_mysql.rb
new file mode 100644
index 00000000000..9bb960082f5
--- /dev/null
+++ b/db/migrate/20151020173516_ci_limits_to_mysql.rb
@@ -0,0 +1,9 @@
+class CiLimitsToMysql < ActiveRecord::Migration
+ def change
+ return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/
+
+ # CI
+ change_column :ci_builds, :trace, :text, limit: 1073741823
+ change_column :ci_commits, :push_data, :text, limit: 16777215
+ end
+end
diff --git a/db/migrate/20151020173906_add_ci_builds_index_for_status.rb b/db/migrate/20151020173906_add_ci_builds_index_for_status.rb
new file mode 100644
index 00000000000..c3f0e0606da
--- /dev/null
+++ b/db/migrate/20151020173906_add_ci_builds_index_for_status.rb
@@ -0,0 +1,5 @@
+class AddCiBuildsIndexForStatus < ActiveRecord::Migration
+ def change
+ add_index :ci_builds, [:commit_id, :status, :type]
+ end
+end
diff --git a/db/migrate/20151023112551_fail_build_with_empty_name.rb b/db/migrate/20151023112551_fail_build_with_empty_name.rb
new file mode 100644
index 00000000000..41c0f0649cd
--- /dev/null
+++ b/db/migrate/20151023112551_fail_build_with_empty_name.rb
@@ -0,0 +1,8 @@
+class FailBuildWithEmptyName < ActiveRecord::Migration
+ def up
+ execute("UPDATE ci_builds SET status='failed' WHERE (name IS NULL OR name='') AND status='pending'")
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20151023144219_remove_satellites.rb b/db/migrate/20151023144219_remove_satellites.rb
new file mode 100644
index 00000000000..e73f300028a
--- /dev/null
+++ b/db/migrate/20151023144219_remove_satellites.rb
@@ -0,0 +1,17 @@
+require 'fileutils'
+
+class RemoveSatellites < ActiveRecord::Migration
+ def up
+ satellites = Gitlab.config['satellites']
+ return if satellites.nil?
+
+ satellites_path = satellites['path']
+ return if satellites_path.nil?
+
+ FileUtils.rm_rf(satellites_path)
+ end
+
+ def down
+ # Do nothing
+ end
+end
diff --git a/db/migrate/20151026182941_add_project_path_index.rb b/db/migrate/20151026182941_add_project_path_index.rb
new file mode 100644
index 00000000000..a62fe199d70
--- /dev/null
+++ b/db/migrate/20151026182941_add_project_path_index.rb
@@ -0,0 +1,9 @@
+class AddProjectPathIndex < ActiveRecord::Migration
+ def up
+ add_index :projects, :path
+ end
+
+ def down
+ remove_index :projects, :path
+ end
+end
diff --git a/db/migrate/20151103001141_add_public_to_group.rb b/db/migrate/20151103001141_add_public_to_group.rb
new file mode 100644
index 00000000000..635346300c2
--- /dev/null
+++ b/db/migrate/20151103001141_add_public_to_group.rb
@@ -0,0 +1,5 @@
+class AddPublicToGroup < ActiveRecord::Migration
+ def change
+ add_column :namespaces, :public, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20151103133339_add_shared_runners_setting.rb b/db/migrate/20151103133339_add_shared_runners_setting.rb
new file mode 100644
index 00000000000..4231dfd5c2e
--- /dev/null
+++ b/db/migrate/20151103133339_add_shared_runners_setting.rb
@@ -0,0 +1,5 @@
+class AddSharedRunnersSetting < ActiveRecord::Migration
+ def up
+ add_column :application_settings, :shared_runners_enabled, :boolean, default: true, null: false
+ end
+end
diff --git a/db/migrate/20151103134857_create_lfs_objects.rb b/db/migrate/20151103134857_create_lfs_objects.rb
new file mode 100644
index 00000000000..2d04c170a88
--- /dev/null
+++ b/db/migrate/20151103134857_create_lfs_objects.rb
@@ -0,0 +1,10 @@
+class CreateLfsObjects < ActiveRecord::Migration
+ def change
+ create_table :lfs_objects do |t|
+ t.string :oid, null: false, unique: true
+ t.integer :size, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20151103134958_create_lfs_objects_projects.rb b/db/migrate/20151103134958_create_lfs_objects_projects.rb
new file mode 100644
index 00000000000..f3f58b931ec
--- /dev/null
+++ b/db/migrate/20151103134958_create_lfs_objects_projects.rb
@@ -0,0 +1,12 @@
+class CreateLfsObjectsProjects < ActiveRecord::Migration
+ def change
+ create_table :lfs_objects_projects do |t|
+ t.integer :lfs_object_id, null: false
+ t.integer :project_id, null: false
+
+ t.timestamps
+ end
+
+ add_index :lfs_objects_projects, :project_id
+ end
+end
diff --git a/db/migrate/20151104105513_add_file_to_lfs_objects.rb b/db/migrate/20151104105513_add_file_to_lfs_objects.rb
new file mode 100644
index 00000000000..7c57f3f0df6
--- /dev/null
+++ b/db/migrate/20151104105513_add_file_to_lfs_objects.rb
@@ -0,0 +1,5 @@
+class AddFileToLfsObjects < ActiveRecord::Migration
+ def change
+ add_column :lfs_objects, :file, :string
+ end
+end
diff --git a/db/migrate/20151105094515_create_releases.rb b/db/migrate/20151105094515_create_releases.rb
new file mode 100644
index 00000000000..fe4608c6662
--- /dev/null
+++ b/db/migrate/20151105094515_create_releases.rb
@@ -0,0 +1,14 @@
+class CreateReleases < ActiveRecord::Migration
+ def change
+ create_table :releases do |t|
+ t.string :tag
+ t.text :description
+ t.integer :project_id
+
+ t.timestamps
+ end
+
+ add_index :releases, :project_id
+ add_index :releases, [:project_id, :tag]
+ end
+end
diff --git a/db/migrate/20151106000015_add_is_award_to_notes.rb b/db/migrate/20151106000015_add_is_award_to_notes.rb
new file mode 100644
index 00000000000..02b271637e9
--- /dev/null
+++ b/db/migrate/20151106000015_add_is_award_to_notes.rb
@@ -0,0 +1,6 @@
+class AddIsAwardToNotes < ActiveRecord::Migration
+ def change
+ add_column :notes, :is_award, :boolean, default: false, null: false
+ add_index :notes, :is_award
+ end
+end
diff --git a/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb b/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb
new file mode 100644
index 00000000000..01d8c0f043e
--- /dev/null
+++ b/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb
@@ -0,0 +1,5 @@
+class AddMaxArtifactsSizeToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :max_artifacts_size, :integer, default: 100, null: false
+ end
+end
diff --git a/db/migrate/20151109134526_add_issues_state_index.rb b/db/migrate/20151109134526_add_issues_state_index.rb
new file mode 100644
index 00000000000..1c4d2e30171
--- /dev/null
+++ b/db/migrate/20151109134526_add_issues_state_index.rb
@@ -0,0 +1,5 @@
+class AddIssuesStateIndex < ActiveRecord::Migration
+ def change
+ add_index :issues, :state
+ end
+end
diff --git a/db/migrate/20151109134916_add_projects_visibility_level_index.rb b/db/migrate/20151109134916_add_projects_visibility_level_index.rb
new file mode 100644
index 00000000000..600b4bafd98
--- /dev/null
+++ b/db/migrate/20151109134916_add_projects_visibility_level_index.rb
@@ -0,0 +1,5 @@
+class AddProjectsVisibilityLevelIndex < ActiveRecord::Migration
+ def change
+ add_index :projects, :visibility_level
+ end
+end
diff --git a/db/migrate/20151110125604_add_import_error_to_project.rb b/db/migrate/20151110125604_add_import_error_to_project.rb
new file mode 100644
index 00000000000..7fc990f8d0a
--- /dev/null
+++ b/db/migrate/20151110125604_add_import_error_to_project.rb
@@ -0,0 +1,5 @@
+class AddImportErrorToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :import_error, :text
+ end
+end
diff --git a/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb
new file mode 100644
index 00000000000..d10f1f6e605
--- /dev/null
+++ b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb
@@ -0,0 +1,6 @@
+class AddIndexForLfsOidAndSize < ActiveRecord::Migration
+ def change
+ add_index :lfs_objects, :oid
+ add_index :lfs_objects, [:oid, :size]
+ end
+end
diff --git a/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb b/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb
new file mode 100644
index 00000000000..41b93da0a86
--- /dev/null
+++ b/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb
@@ -0,0 +1,7 @@
+class AddUniqueForLfsOidIndex < ActiveRecord::Migration
+ def change
+ remove_index :lfs_objects, :oid
+ remove_index :lfs_objects, [:oid, :size]
+ add_index :lfs_objects, :oid, unique: true
+ end
+end
diff --git a/db/migrate/20151118162244_add_projects_public_index.rb b/db/migrate/20151118162244_add_projects_public_index.rb
new file mode 100644
index 00000000000..fded70e3c0c
--- /dev/null
+++ b/db/migrate/20151118162244_add_projects_public_index.rb
@@ -0,0 +1,5 @@
+class AddProjectsPublicIndex < ActiveRecord::Migration
+ def change
+ add_index :namespaces, :public
+ end
+end
diff --git a/db/migrate/20151203162133_add_hide_project_limit_to_users.rb b/db/migrate/20151203162133_add_hide_project_limit_to_users.rb
new file mode 100644
index 00000000000..6ffadfa1894
--- /dev/null
+++ b/db/migrate/20151203162133_add_hide_project_limit_to_users.rb
@@ -0,0 +1,5 @@
+class AddHideProjectLimitToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :hide_project_limit, :boolean, default: false
+ end
+end
diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb
index 73605d4c5e3..2b7afae6d7c 100644
--- a/db/migrate/limits_to_mysql.rb
+++ b/db/migrate/limits_to_mysql.rb
@@ -6,9 +6,5 @@ class LimitsToMysql < ActiveRecord::Migration
change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
change_column :snippets, :content, :text, limit: 2147483647
change_column :notes, :st_diff, :text, limit: 2147483647
-
- # CI
- change_column :ci_builds, :trace, :text, limit: 1073741823
- change_column :ci_commits, :push_data, :text, limit: 16777215
end
end
diff --git a/db/schema.rb b/db/schema.rb
index 01ccda7a75e..fb59e187625 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,12 +11,12 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20150920161119) do
+ActiveRecord::Schema.define(version: 20151203162133) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
- create_table "abuse_reports", force: true do |t|
+ create_table "abuse_reports", force: :cascade do |t|
t.integer "reporter_id"
t.integer "user_id"
t.text "message"
@@ -24,7 +24,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
t.datetime "updated_at"
end
- create_table "application_settings", force: true do |t|
+ create_table "application_settings", force: :cascade do |t|
t.integer "default_projects_limit"
t.boolean "signup_enabled"
t.boolean "signin_enabled"
@@ -46,10 +46,12 @@ ActiveRecord::Schema.define(version: 20150920161119) do
t.integer "session_expire_delay", default: 10080, null: false
t.text "import_sources"
t.text "help_page_text"
- t.boolean "ci_enabled", default: true, null: false
+ t.string "admin_notification_email"
+ t.boolean "shared_runners_enabled", default: true, null: false
+ t.integer "max_artifacts_size", default: 100, null: false
end
- create_table "audit_events", force: true do |t|
+ create_table "audit_events", force: :cascade do |t|
t.integer "author_id", null: false
t.string "type", null: false
t.integer "entity_id", null: false
@@ -63,7 +65,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree
- create_table "broadcast_messages", force: true do |t|
+ create_table "broadcast_messages", force: :cascade do |t|
t.text "message", null: false
t.datetime "starts_at"
t.datetime "ends_at"
@@ -74,14 +76,14 @@ ActiveRecord::Schema.define(version: 20150920161119) do
t.string "font"
end
- create_table "ci_application_settings", force: true do |t|
+ create_table "ci_application_settings", force: :cascade do |t|
t.boolean "all_broken_builds"
t.boolean "add_pusher"
t.datetime "created_at"
t.datetime "updated_at"
end
- create_table "ci_builds", force: true do |t|
+ create_table "ci_builds", force: :cascade do |t|
t.integer "project_id"
t.string "status"
t.datetime "finished_at"
@@ -100,14 +102,28 @@ ActiveRecord::Schema.define(version: 20150920161119) do
t.boolean "allow_failure", default: false, null: false
t.string "stage"
t.integer "trigger_request_id"
+ t.integer "stage_idx"
+ t.boolean "tag"
+ t.string "ref"
+ t.integer "user_id"
+ t.string "type"
+ t.string "target_url"
+ t.string "description"
+ t.text "artifacts_file"
end
+ add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
+ add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
+ add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
+ add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree
add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
+ add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
+ add_index "ci_builds", ["type"], name: "index_ci_builds_on_type", using: :btree
- create_table "ci_commits", force: true do |t|
+ create_table "ci_commits", force: :cascade do |t|
t.integer "project_id"
t.string "ref"
t.string "sha"
@@ -115,18 +131,20 @@ ActiveRecord::Schema.define(version: 20150920161119) do
t.text "push_data"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "tag", default: false
+ t.boolean "tag", default: false
t.text "yaml_errors"
t.datetime "committed_at"
+ t.integer "gl_project_id"
end
+ add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree
add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree
add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree
add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree
add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree
add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree
- create_table "ci_events", force: true do |t|
+ create_table "ci_events", force: :cascade do |t|
t.integer "project_id"
t.integer "user_id"
t.integer "is_admin"
@@ -139,7 +157,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree
add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree
- create_table "ci_jobs", force: true do |t|
+ create_table "ci_jobs", force: :cascade do |t|
t.integer "project_id", null: false
t.text "commands"
t.boolean "active", default: true, null: false
@@ -156,8 +174,8 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree
add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree
- create_table "ci_projects", force: true do |t|
- t.string "name", null: false
+ create_table "ci_projects", force: :cascade do |t|
+ t.string "name"
t.integer "timeout", default: 3600, null: false
t.datetime "created_at"
t.datetime "updated_at"
@@ -179,7 +197,10 @@ ActiveRecord::Schema.define(version: 20150920161119) do
t.text "generated_yaml_config"
end
- create_table "ci_runner_projects", force: true do |t|
+ add_index "ci_projects", ["gitlab_id"], name: "index_ci_projects_on_gitlab_id", using: :btree
+ add_index "ci_projects", ["shared_runners_enabled"], name: "index_ci_projects_on_shared_runners_enabled", using: :btree
+
+ create_table "ci_runner_projects", force: :cascade do |t|
t.integer "runner_id", null: false
t.integer "project_id", null: false
t.datetime "created_at"
@@ -189,7 +210,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
- create_table "ci_runners", force: true do |t|
+ create_table "ci_runners", force: :cascade do |t|
t.string "token"
t.datetime "created_at"
t.datetime "updated_at"
@@ -204,7 +225,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
t.string "architecture"
end
- create_table "ci_services", force: true do |t|
+ create_table "ci_services", force: :cascade do |t|
t.string "type"
t.string "title"
t.integer "project_id", null: false
@@ -216,7 +237,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree
- create_table "ci_sessions", force: true do |t|
+ create_table "ci_sessions", force: :cascade do |t|
t.string "session_id", null: false
t.text "data"
t.datetime "created_at"
@@ -226,7 +247,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree
add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree
- create_table "ci_taggings", force: true do |t|
+ create_table "ci_taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
t.string "taggable_type"
@@ -239,14 +260,14 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree
add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
- create_table "ci_tags", force: true do |t|
+ create_table "ci_tags", force: :cascade do |t|
t.string "name"
t.integer "taggings_count", default: 0
end
add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree
- create_table "ci_trigger_requests", force: true do |t|
+ create_table "ci_trigger_requests", force: :cascade do |t|
t.integer "trigger_id", null: false
t.text "variables"
t.datetime "created_at"
@@ -254,7 +275,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
t.integer "commit_id"
end
- create_table "ci_triggers", force: true do |t|
+ create_table "ci_triggers", force: :cascade do |t|
t.string "token"
t.integer "project_id", null: false
t.datetime "deleted_at"
@@ -264,7 +285,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree
- create_table "ci_variables", force: true do |t|
+ create_table "ci_variables", force: :cascade do |t|
t.integer "project_id", null: false
t.string "key"
t.text "value"
@@ -275,14 +296,14 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
- create_table "ci_web_hooks", force: true do |t|
+ create_table "ci_web_hooks", force: :cascade do |t|
t.string "url", null: false
t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
- create_table "deploy_keys_projects", force: true do |t|
+ create_table "deploy_keys_projects", force: :cascade do |t|
t.integer "deploy_key_id", null: false
t.integer "project_id", null: false
t.datetime "created_at"
@@ -291,7 +312,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
- create_table "emails", force: true do |t|
+ create_table "emails", force: :cascade do |t|
t.integer "user_id", null: false
t.string "email", null: false
t.datetime "created_at"
@@ -301,7 +322,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree
add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
- create_table "events", force: true do |t|
+ create_table "events", force: :cascade do |t|
t.string "target_type"
t.integer "target_id"
t.string "title"
@@ -320,7 +341,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree
add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree
- create_table "forked_project_links", force: true do |t|
+ create_table "forked_project_links", force: :cascade do |t|
t.integer "forked_to_project_id", null: false
t.integer "forked_from_project_id", null: false
t.datetime "created_at"
@@ -329,7 +350,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
- create_table "identities", force: true do |t|
+ create_table "identities", force: :cascade do |t|
t.string "extern_uid"
t.string "provider"
t.integer "user_id"
@@ -340,7 +361,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "identities", ["created_at", "id"], name: "index_identities_on_created_at_and_id", using: :btree
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
- create_table "issues", force: true do |t|
+ create_table "issues", force: :cascade do |t|
t.string "title"
t.integer "assignee_id"
t.integer "author_id"
@@ -363,9 +384,10 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
+ add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
- create_table "keys", force: true do |t|
+ create_table "keys", force: :cascade do |t|
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
@@ -379,7 +401,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree
add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
- create_table "label_links", force: true do |t|
+ create_table "label_links", force: :cascade do |t|
t.integer "label_id"
t.integer "target_id"
t.string "target_type"
@@ -390,7 +412,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "label_links", ["label_id"], name: "index_label_links_on_label_id", using: :btree
add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree
- create_table "labels", force: true do |t|
+ create_table "labels", force: :cascade do |t|
t.string "title"
t.string "color"
t.integer "project_id"
@@ -401,7 +423,26 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
- create_table "members", force: true do |t|
+ create_table "lfs_objects", force: :cascade do |t|
+ t.string "oid", null: false
+ t.integer "size", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "file"
+ end
+
+ add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree
+
+ create_table "lfs_objects_projects", force: :cascade do |t|
+ t.integer "lfs_object_id", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
+
+ create_table "members", force: :cascade do |t|
t.integer "access_level", null: false
t.integer "source_id", null: false
t.string "source_type", null: false
@@ -423,7 +464,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "members", ["type"], name: "index_members_on_type", using: :btree
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
- create_table "merge_request_diffs", force: true do |t|
+ create_table "merge_request_diffs", force: :cascade do |t|
t.string "state"
t.text "st_commits"
t.text "st_diffs"
@@ -434,7 +475,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
- create_table "merge_requests", force: true do |t|
+ create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
t.string "source_branch", null: false
t.integer "source_project_id", null: false
@@ -452,6 +493,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
t.integer "position", default: 0
t.datetime "locked_at"
t.integer "updated_by_id"
+ t.string "merge_error"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -465,7 +507,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
- create_table "milestones", force: true do |t|
+ create_table "milestones", force: :cascade do |t|
t.string "title", null: false
t.integer "project_id", null: false
t.text "description"
@@ -481,24 +523,26 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
- create_table "namespaces", force: true do |t|
- t.string "name", null: false
- t.string "path", null: false
+ create_table "namespaces", force: :cascade do |t|
+ t.string "name", null: false
+ t.string "path", null: false
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "type"
- t.string "description", default: "", null: false
+ t.string "description", default: "", null: false
t.string "avatar"
+ t.boolean "public", default: false
end
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
+ add_index "namespaces", ["public"], name: "index_namespaces_on_public", using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
- create_table "notes", force: true do |t|
+ create_table "notes", force: :cascade do |t|
t.text "note"
t.string "noteable_type"
t.integer "author_id"
@@ -512,19 +556,22 @@ ActiveRecord::Schema.define(version: 20150920161119) do
t.boolean "system", default: false, null: false
t.text "st_diff"
t.integer "updated_by_id"
+ t.boolean "is_award", default: false, null: false
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
+ add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree
+ add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
- create_table "oauth_access_grants", force: true do |t|
+ create_table "oauth_access_grants", force: :cascade do |t|
t.integer "resource_owner_id", null: false
t.integer "application_id", null: false
t.string "token", null: false
@@ -537,7 +584,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree
- create_table "oauth_access_tokens", force: true do |t|
+ create_table "oauth_access_tokens", force: :cascade do |t|
t.integer "resource_owner_id"
t.integer "application_id"
t.string "token", null: false
@@ -552,7 +599,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree
add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree
- create_table "oauth_applications", force: true do |t|
+ create_table "oauth_applications", force: :cascade do |t|
t.string "name", null: false
t.string "uid", null: false
t.string "secret", null: false
@@ -567,12 +614,12 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
- create_table "project_import_data", force: true do |t|
+ create_table "project_import_data", force: :cascade do |t|
t.integer "project_id"
t.text "data"
end
- create_table "projects", force: true do |t|
+ create_table "projects", force: :cascade do |t|
t.string "name"
t.string "path"
t.text "description"
@@ -598,15 +645,18 @@ ActiveRecord::Schema.define(version: 20150920161119) do
t.string "import_type"
t.string "import_source"
t.integer "commit_count", default: 0
+ t.text "import_error"
end
add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree
add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
+ add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
+ add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
- create_table "protected_branches", force: true do |t|
+ create_table "protected_branches", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at"
@@ -616,7 +666,18 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
- create_table "sent_notifications", force: true do |t|
+ create_table "releases", force: :cascade do |t|
+ t.string "tag"
+ t.text "description"
+ t.integer "project_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
+ add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree
+
+ create_table "sent_notifications", force: :cascade do |t|
t.integer "project_id"
t.integer "noteable_id"
t.string "noteable_type"
@@ -628,7 +689,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
- create_table "services", force: true do |t|
+ create_table "services", force: :cascade do |t|
t.string "type"
t.string "title"
t.integer "project_id"
@@ -646,8 +707,9 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
+ add_index "services", ["template"], name: "index_services_on_template", using: :btree
- create_table "snippets", force: true do |t|
+ create_table "snippets", force: :cascade do |t|
t.string "title"
t.text "content"
t.integer "author_id", null: false
@@ -667,7 +729,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
- create_table "subscriptions", force: true do |t|
+ create_table "subscriptions", force: :cascade do |t|
t.integer "user_id"
t.integer "subscribable_id"
t.string "subscribable_type"
@@ -678,7 +740,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree
- create_table "taggings", force: true do |t|
+ create_table "taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
t.string "taggable_type"
@@ -691,14 +753,14 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true, using: :btree
add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
- create_table "tags", force: true do |t|
+ create_table "tags", force: :cascade do |t|
t.string "name"
t.integer "taggings_count", default: 0
end
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
- create_table "users", force: true do |t|
+ create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
@@ -751,6 +813,8 @@ ActiveRecord::Schema.define(version: 20150920161119) do
t.integer "dashboard", default: 0
t.integer "project_view", default: 0
t.integer "consumed_timestep"
+ t.integer "layout", default: 0
+ t.boolean "hide_project_limit", default: false
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -763,7 +827,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
add_index "users", ["username"], name: "index_users_on_username", using: :btree
- create_table "users_star_projects", force: true do |t|
+ create_table "users_star_projects", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "user_id", null: false
t.datetime "created_at"
@@ -774,7 +838,7 @@ ActiveRecord::Schema.define(version: 20150920161119) do
add_index "users_star_projects", ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree
add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree
- create_table "web_hooks", force: true do |t|
+ create_table "web_hooks", force: :cascade do |t|
t.string "url"
t.integer "project_id"
t.datetime "created_at"
diff --git a/doc/README.md b/doc/README.md
index a0ff856ebb6..58ab5dd08e0 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -17,20 +17,22 @@
## CI Documentation
-+ [Quick Start](ci/quick_start/README.md)
-+ [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md)
-+ [Configuring runner](ci/runners/README.md)
-+ [Configuring deployment](ci/deployment/README.md)
-+ [Using Docker Images](ci/docker/using_docker_images.md)
-+ [Using Docker Build](ci/docker/using_docker_build.md)
-+ [Using Variables](ci/variables/README.md)
+- [Quick Start](ci/quick_start/README.md)
+- [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md)
+- [Configuring runner](ci/runners/README.md)
+- [Configuring deployment](ci/deployment/README.md)
+- [Using Docker Images](ci/docker/using_docker_images.md)
+- [Using Docker Build](ci/docker/using_docker_build.md)
+- [Using Variables](ci/variables/README.md)
+- [User permissions](ci/permissions/README.md)
+- [API](ci/api/README.md)
### CI Examples
-+ [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md)
-+ [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md)
-+ [Test Clojure applications](ci/examples/test-clojure-application.md)
-+ Help your favorite programming language and GitLab by sending a merge request with a guide for that language.
+- [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md)
+- [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md)
+- [Test Clojure applications](ci/examples/test-clojure-application.md)
+- Help your favorite programming language and GitLab by sending a merge request with a guide for that language.
## Administrator documentation
@@ -48,11 +50,7 @@
- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page.
- [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails.
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
-
-### Administrator documentation
-
-+ [User permissions](permissions/permissions.md)
-+ [API](api/README.md)
+- [Git LFS configuration](workflow/lfs/lfs_administration.md)
## Contributor documentation
diff --git a/doc/api/README.md b/doc/api/README.md
index 6b8528de50c..25a31b235cc 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -10,6 +10,7 @@
- [Repositories](repositories.md)
- [Repository Files](repository_files.md)
- [Commits](commits.md)
+- [Tags](tags.md)
- [Branches](branches.md)
- [Merge Requests](merge_requests.md)
- [Issues](issues.md)
diff --git a/doc/api/commits.md b/doc/api/commits.md
index eb8d6a43592..93d62b751e6 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -22,7 +22,8 @@ Parameters:
"author_name": "Dmitriy Zaporozhets",
"author_email": "dzaporozhets@sphereconsultinginc.com",
"created_at": "2012-09-20T11:50:22+03:00",
- "message": "Replace sanitize with escape once"
+ "message": "Replace sanitize with escape once",
+ "allow_failure": false
},
{
"id": "6104942438c14ec7bd21c6cd5bd995272b3faff6",
@@ -31,7 +32,8 @@ Parameters:
"author_name": "randx",
"author_email": "dmitriy.zaporozhets@gmail.com",
"created_at": "2012-09-20T09:06:12+03:00",
- "message": "Sanitize for network graph"
+ "message": "Sanitize for network graph",
+ "allow_failure": false
}
]
```
@@ -62,7 +64,8 @@ Parameters:
"authored_date": "2012-09-20T09:06:12+03:00",
"parent_ids": [
"ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
- ]
+ ],
+ "status": "running"
}
```
@@ -156,3 +159,84 @@ Parameters:
"line_type": "new"
}
```
+
+## Get the status of a commit
+
+Get the statuses of a commit in a project.
+
+```
+GET /projects/:id/repository/commits/:sha/statuses
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `sha` (required) - The commit SHA
+- `ref` (optional) - Filter by ref name, it can be branch or tag
+- `stage` (optional) - Filter by stage
+- `name` (optional) - Filer by status name, eg. jenkins
+- `all` (optional) - The flag to return all statuses, not only latest ones
+
+```json
+[
+ {
+ "id": 13,
+ "sha": "b0b3a907f41409829b307a28b82fdbd552ee5a27",
+ "ref": "test",
+ "status": "success",
+ "name": "ci/jenkins",
+ "target_url": "http://jenkins/project/url",
+ "description": "Jenkins success",
+ "created_at": "2015-10-12T09:47:16.250Z",
+ "started_at": "2015-10-12T09:47:16.250Z",
+ "finished_at": "2015-10-12T09:47:16.262Z",
+ "author": {
+ "id": 1,
+ "username": "admin",
+ "email": "admin@local.host",
+ "name": "Administrator",
+ "blocked": false,
+ "created_at": "2012-04-29T08:46:00Z"
+ }
+ }
+]
+```
+
+## Post the status to commit
+
+Adds or updates a status of a commit.
+
+```
+POST /projects/:id/statuses/:sha
+```
+
+- `id` (required) - The ID of a project
+- `sha` (required) - The commit SHA
+- `state` (required) - The state of the status. Can be: pending, running, success, failed, canceled
+- `ref` (optional) - The ref (branch or tag) to which the status refers
+- `name` or `context` (optional) - The label to differentiate this status from the status of other systems. Default: "default"
+- `target_url` (optional) - The target URL to associate with this status
+- `description` (optional) - The short description of the status
+
+```json
+{
+ "id": 13,
+ "sha": "b0b3a907f41409829b307a28b82fdbd552ee5a27",
+ "ref": "test",
+ "status": "success",
+ "name": "ci/jenkins",
+ "target_url": "http://jenkins/project/url",
+ "description": "Jenkins success",
+ "created_at": "2015-10-12T09:47:16.250Z",
+ "started_at": "2015-10-12T09:47:16.250Z",
+ "finished_at": "2015-10-12T09:47:16.262Z",
+ "author": {
+ "id": 1,
+ "username": "admin",
+ "email": "admin@local.host",
+ "name": "Administrator",
+ "blocked": false,
+ "created_at": "2012-04-29T08:46:00Z"
+ }
+}
+```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index bb551fc67f7..82f2cef969f 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -3,8 +3,9 @@
## List merge requests
Get all merge requests for this project.
-The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`).
-The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests.
+The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`).
+The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. With GitLab 8.2 the return fields `upvotes` and
+`downvotes` are deprecated and always return `0`.
```
GET /projects/:id/merge_requests
@@ -57,7 +58,7 @@ Parameters:
## Get single MR
-Shows information about a single merge request.
+Shows information about a single merge request. With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and always return `0`.
```
GET /projects/:id/merge_request/:merge_request_id
@@ -100,9 +101,48 @@ Parameters:
}
```
+## Get single MR commits
+
+Get a list of merge request commits.
+
+```
+GET /projects/:id/merge_request/:merge_request_id/commits
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `merge_request_id` (required) - The ID of MR
+
+
+```json
+[
+ {
+ "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+ "short_id": "ed899a2f4b5",
+ "title": "Replace sanitize with escape once",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dzaporozhets@sphereconsultinginc.com",
+ "created_at": "2012-09-20T11:50:22+03:00",
+ "message": "Replace sanitize with escape once"
+ },
+ {
+ "id": "6104942438c14ec7bd21c6cd5bd995272b3faff6",
+ "short_id": "6104942438c",
+ "title": "Sanitize for network graph",
+ "author_name": "randx",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "created_at": "2012-09-20T09:06:12+03:00",
+ "message": "Sanitize for network graph"
+ }
+]
+```
+
## Get single MR changes
-Shows information about the merge request including its files and changes
+Shows information about the merge request including its files and changes.
+With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and
+always return `0`.
```
GET /projects/:id/merge_request/:merge_request_id/changes
@@ -156,7 +196,7 @@ Parameters:
"updated_at": "2015-02-02T19:49:26.013Z",
"due_date": null
},
- "files": [
+ "changes": [
{
"old_path": "VERSION",
"new_path": "VERSION",
@@ -173,7 +213,8 @@ Parameters:
## Create MR
-Creates a new merge request.
+Creates a new merge request. With GitLab 8.2 the return fields `upvotes` and `
+downvotes` are deprecated and always return `0`.
```
POST /projects/:id/merge_requests
@@ -188,6 +229,7 @@ Parameters:
- `title` (required) - Title of MR
- `description` (optional) - Description of MR
- `target_project_id` (optional) - The target project (numeric id)
+- `labels` (optional) - Labels for MR as a comma-separated list
```json
{
@@ -224,7 +266,8 @@ If an error occurs, an error number and a message explaining the reason is retur
## Update MR
-Updates an existing merge request. You can change the target branch, title, or even close the MR.
+Updates an existing merge request. You can change the target branch, title, or even close the MR. With GitLab 8.2 the return fields `upvotes` and `downvotes`
+are deprecated and always return `0`.
```
PUT /projects/:id/merge_request/:merge_request_id
@@ -239,6 +282,7 @@ Parameters:
- `title` - Title of MR
- `description` - Description of MR
- `state_event` - New state (close|reopen|merge)
+- `labels` (optional) - Labels for MR as a comma-separated list
```json
{
@@ -274,7 +318,8 @@ If an error occurs, an error number and a message explaining the reason is retur
## Accept MR
-Merge changes submitted with MR using this API.
+Merge changes submitted with MR using this API. With GitLab 8.2 the return
+fields `upvotes` and `downvotes` are deprecated and always return `0`.
If merge success you get `200 OK`.
diff --git a/doc/api/notes.md b/doc/api/notes.md
index c683cb883d4..4d7ef288df8 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -6,7 +6,8 @@ Notes are comments on snippets, issues or merge requests.
### List project issue notes
-Gets a list of all notes for a single issue.
+Gets a list of all notes for a single issue. With GitLab 8.2 the return fields
+`upvote` and `downvote` are deprecated and always return `false`.
```
GET /projects/:id/issues/:issue_id/notes
@@ -34,7 +35,9 @@ Parameters:
"created_at": "2013-10-02T09:22:45Z",
"system": true,
"upvote": false,
- "downvote": false
+ "downvote": false,
+ "noteable_id": 377,
+ "noteable_type": "Issue"
},
{
"id": 305,
@@ -49,9 +52,11 @@ Parameters:
"created_at": "2013-09-30T13:46:01Z"
},
"created_at": "2013-10-02T09:56:03Z",
- "system": false,
+ "system": true,
"upvote": false,
- "downvote": false
+ "downvote": false,
+ "noteable_id": 121,
+ "noteable_type": "Issue"
}
]
```
@@ -218,7 +223,12 @@ Parameters:
"state": "active",
"created_at": "2013-09-30T13:46:01Z"
},
- "created_at": "2013-10-02T08:57:14Z"
+ "created_at": "2013-10-02T08:57:14Z",
+ "system": false,
+ "upvote": false,
+ "downvote": false,
+ "noteable_id": 2,
+ "noteable_type": "MergeRequest"
}
```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 96485857035..42919a312ae 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -60,6 +60,7 @@ Parameters:
"path_with_namespace": "diaspora/diaspora-client",
"issues_enabled": true,
"merge_requests_enabled": true,
+ "builds_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"created_at": "2013-09-30T13: 46: 02Z",
@@ -101,6 +102,7 @@ Parameters:
"path_with_namespace": "brightbox/puppet",
"issues_enabled": true,
"merge_requests_enabled": true,
+ "builds_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"created_at": "2013-09-30T13:46:02Z",
@@ -191,6 +193,7 @@ Parameters:
"path_with_namespace": "diaspora/diaspora-project-site",
"issues_enabled": true,
"merge_requests_enabled": true,
+ "builds_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"created_at": "2013-09-30T13: 46: 02Z",
@@ -242,9 +245,17 @@ Parameters:
"target_id": 830,
"target_type": "Issue",
"author_id": 1,
- "author_username": "john",
"data": null,
- "target_title": "Public project search field"
+ "target_title": "Public project search field",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "author_username": "root"
},
{
"title": null,
@@ -253,6 +264,14 @@ Parameters:
"target_id": null,
"target_type": null,
"author_id": 1,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/u/root"
+ },
"author_username": "john",
"data": {
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
@@ -289,9 +308,56 @@ Parameters:
"target_id": 840,
"target_type": "Issue",
"author_id": 1,
- "author_username": "john",
"data": null,
- "target_title": "Finish & merge Code search PR"
+ "target_title": "Finish & merge Code search PR",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "author_username": "root"
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "commented on",
+ "target_id": 1312,
+ "target_type": "Note",
+ "author_id": 1,
+ "data": null,
+ "target_title": null,
+ "created_at": "2015-12-04T10:33:58.089Z",
+ "note": {
+ "id": 1312,
+ "body": "What an awesome day!",
+ "attachment": null,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "created_at": "2015-12-04T10:33:56.698Z",
+ "system": false,
+ "upvote": false,
+ "downvote": false,
+ "noteable_id": 377,
+ "noteable_type": "Issue"
+ },
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "author_username": "root"
}
]
```
@@ -312,6 +378,7 @@ Parameters:
- `description` (optional) - short project description
- `issues_enabled` (optional)
- `merge_requests_enabled` (optional)
+- `builds_enabled` (optional)
- `wiki_enabled` (optional)
- `snippets_enabled` (optional)
- `public` (optional) - if `true` same as setting visibility_level = 20
@@ -334,6 +401,7 @@ Parameters:
- `default_branch` (optional) - 'master' by default
- `issues_enabled` (optional)
- `merge_requests_enabled` (optional)
+- `builds_enabled` (optional)
- `wiki_enabled` (optional)
- `snippets_enabled` (optional)
- `public` (optional) - if `true` same as setting visibility_level = 20
@@ -357,6 +425,7 @@ Parameters:
- `default_branch` (optional)
- `issues_enabled` (optional)
- `merge_requests_enabled` (optional)
+- `builds_enabled` (optional)
- `wiki_enabled` (optional)
- `snippets_enabled` (optional)
- `public` (optional) - if `true` same as setting visibility_level = 20
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 33167453802..b6cca5d4e2a 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -1,79 +1,5 @@
# Repositories
-## List project repository tags
-
-Get a list of repository tags from a project, sorted by name in reverse alphabetical order.
-
-```
-GET /projects/:id/repository/tags
-```
-
-Parameters:
-
-- `id` (required) - The ID of a project
-
-```json
-[
- {
- "commit": {
- "author_name": "John Smith",
- "author_email": "john@example.com",
- "authored_date": "2012-05-28T04:42:42-07:00",
- "committed_date": "2012-05-28T04:42:42-07:00",
- "committer_name": "Jack Smith",
- "committer_email": "jack@example.com",
- "id": "2695effb5807a22ff3d138d593fd856244e155e7",
- "message": "Initial commit",
- "parents_ids": [
- "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
- ]
- },
- "name": "v1.0.0",
- "message": null
- }
-]
-```
-
-## Create a new tag
-
-Creates new tag in the repository that points to the supplied ref.
-
-```
-POST /projects/:id/repository/tags
-```
-
-Parameters:
-
-- `id` (required) - The ID of a project
-- `tag_name` (required) - The name of a tag
-- `ref` (required) - Create tag using commit SHA, another tag name, or branch name.
-- `message` (optional) - Creates annotated tag.
-
-```json
-{
- "commit": {
- "author_name": "John Smith",
- "author_email": "john@example.com",
- "authored_date": "2012-05-28T04:42:42-07:00",
- "committed_date": "2012-05-28T04:42:42-07:00",
- "committer_name": "Jack Smith",
- "committer_email": "jack@example.com",
- "id": "2695effb5807a22ff3d138d593fd856244e155e7",
- "message": "Initial commit",
- "parents_ids": [
- "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
- ]
- },
- "name": "v1.0.0",
- "message": null
-}
-```
-The message will be `nil` when creating a lightweight tag otherwise
-it will contain the annotation.
-
-It returns 200 if the operation succeed. In case of an error,
-405 with an explaining error message is returned.
-
## List repository tree
Get a list of repository files and directories in a project.
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 25311b07107..623063f357b 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -23,7 +23,8 @@ Example response:
"content": "IyA9PSBTY2hlbWEgSW5mb3...",
"ref": "master",
"blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83",
- "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50"
+ "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50",
+ "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
}
```
diff --git a/doc/api/tags.md b/doc/api/tags.md
new file mode 100644
index 00000000000..085d387e824
--- /dev/null
+++ b/doc/api/tags.md
@@ -0,0 +1,131 @@
+# Tags
+
+## List project repository tags
+
+Get a list of repository tags from a project, sorted by name in reverse alphabetical order.
+
+```
+GET /projects/:id/repository/tags
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+
+```json
+[
+ {
+ "commit": {
+ "author_name": "John Smith",
+ "author_email": "john@example.com",
+ "authored_date": "2012-05-28T04:42:42-07:00",
+ "committed_date": "2012-05-28T04:42:42-07:00",
+ "committer_name": "Jack Smith",
+ "committer_email": "jack@example.com",
+ "id": "2695effb5807a22ff3d138d593fd856244e155e7",
+ "message": "Initial commit",
+ "parents_ids": [
+ "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
+ ]
+ },
+ "release": {
+ "tag_name": "1.0.0",
+ "description": "Amazing release. Wow"
+ },
+ "name": "v1.0.0",
+ "message": null
+ }
+]
+```
+
+## Create a new tag
+
+Creates a new tag in the repository that points to the supplied ref.
+
+```
+POST /projects/:id/repository/tags
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `tag_name` (required) - The name of a tag
+- `ref` (required) - Create tag using commit SHA, another tag name, or branch name.
+- `message` (optional) - Creates annotated tag.
+- `release_description` (optional) - Add release notes to the git tag and store it in the GitLab database.
+
+```json
+{
+ "commit": {
+ "author_name": "John Smith",
+ "author_email": "john@example.com",
+ "authored_date": "2012-05-28T04:42:42-07:00",
+ "committed_date": "2012-05-28T04:42:42-07:00",
+ "committer_name": "Jack Smith",
+ "committer_email": "jack@example.com",
+ "id": "2695effb5807a22ff3d138d593fd856244e155e7",
+ "message": "Initial commit",
+ "parents_ids": [
+ "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
+ ]
+ },
+ "release": {
+ "tag_name": "1.0.0",
+ "description": "Amazing release. Wow"
+ },
+ "name": "v1.0.0",
+ "message": null
+}
+```
+The message will be `nil` when creating a lightweight tag otherwise
+it will contain the annotation.
+
+It returns 200 if the operation succeed. In case of an error,
+405 with an explaining error message is returned.
+
+
+## Create a new release
+
+Add release notes to the existing git tag. It returns 201 if the release is
+created successfully. If the tag does not exist, 404 is returned. If there
+already exists a release for the given tag, 409 is returned.
+
+```
+POST /projects/:id/repository/tags/:tag_name/release
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `tag_name` (required) - The name of a tag
+- `description` (required) - Release notes with markdown support
+
+```json
+{
+ "tag_name": "1.0.0",
+ "description": "Amazing release. Wow"
+}
+```
+
+## Update a release
+
+Updates the release notes of a given release. It returns 200 if the release is
+successfully updated. If the tag or the release does not exist, it returns 404
+with a proper error message.
+
+```
+PUT /projects/:id/repository/tags/:tag_name/release
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `tag_name` (required) - The name of a tag
+- `description` (required) - Release notes with markdown support
+
+```json
+{
+ "tag_name": "1.0.0",
+ "description": "Amazing release. Wow"
+}
+``` \ No newline at end of file
diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md
index 33c5b172e98..cf9710ede57 100644
--- a/doc/ci/api/README.md
+++ b/doc/ci/api/README.md
@@ -25,7 +25,7 @@ GitLab CI API has 4 authentication methods:
Authentication is done by
sending the `private-token` of a valid user and the `url` of an
-authorized Gitlab instance via a query string along with the API
+authorized GitLab instance via a query string along with the API
request:
GET http://gitlab.example.com/ci/api/v1/projects?private_token=QVy1PB7sTxfy4pqfZM1U&url=http://demo.gitlab.com/
diff --git a/doc/ci/api/projects.md b/doc/ci/api/projects.md
index 54584db0938..74a4c64d000 100644
--- a/doc/ci/api/projects.md
+++ b/doc/ci/api/projects.md
@@ -1,7 +1,7 @@
# Projects API
This API is intended to aid in the setup and configuration of
-projects on Gitlab CI.
+projects on GitLab CI.
__Authentication is done by GitLab user token & GitLab url__
@@ -88,25 +88,23 @@ authorized.
Parameters:
- * `id` (required) - The ID of the Gitlab CI project
+ * `id` (required) - The ID of the GitLab CI project
### Create Project
-Creates a Gitlab CI project using Gitlab project details.
+Creates a GitLab CI project using GitLab project details.
POST /ci/projects
Parameters:
* `name` (required) - The name of the project
- * `gitlab_id` (required) - The ID of the project on the Gitlab instance
- * `path` (required) - The gitlab project path
- * `ssh_url_to_repo` (required) - The gitlab SSH url to the repo
+ * `gitlab_id` (required) - The ID of the project on the GitLab instance
* `default_ref` (optional) - The branch to run on (default to `master`)
### Update Project
-Updates a Gitlab CI project using Gitlab project details that the
+Updates a GitLab CI project using GitLab project details that the
authenticated user has access to.
PUT /ci/projects/:id
@@ -114,20 +112,17 @@ authenticated user has access to.
Parameters:
* `name` - The name of the project
- * `gitlab_id` - The ID of the project on the Gitlab instance
- * `path` - The gitlab project path
- * `ssh_url_to_repo` - The gitlab SSH url to the repo
* `default_ref` - The branch to run on (default to `master`)
### Remove Project
-Removes a Gitlab CI project that the authenticated user has access to.
+Removes a GitLab CI project that the authenticated user has access to.
DELETE /ci/projects/:id
Parameters:
- * `id` (required) - The ID of the Gitlab CI project
+ * `id` (required) - The ID of the GitLab CI project
### Link Project to Runner
@@ -138,8 +133,8 @@ authorized user).
Parameters:
- * `id` (required) - The ID of the Gitlab CI project
- * `runner_id` (required) - The ID of the Gitlab CI runner
+ * `id` (required) - The ID of the GitLab CI project
+ * `runner_id` (required) - The ID of the GitLab CI runner
### Remove Project from Runner
@@ -150,5 +145,5 @@ via authorized user).
Parameters:
- * `id` (required) - The ID of the Gitlab CI project
- * `runner_id` (required) - The ID of the Gitlab CI runner \ No newline at end of file
+ * `id` (required) - The ID of the GitLab CI project
+ * `runner_id` (required) - The ID of the GitLab CI runner \ No newline at end of file
diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md
index e9f88ee066e..c383dc4bcc9 100644
--- a/doc/ci/api/runners.md
+++ b/doc/ci/api/runners.md
@@ -6,7 +6,7 @@
__Authentication is done by GitLab user token & GitLab url__
-Used to get information about all runners registered on the Gitlab CI
+Used to get information about all runners registered on the GitLab CI
instance.
GET /ci/runners
@@ -31,7 +31,7 @@ Returns:
__Authentication is done with a Shared runner registration token or a project Specific runner registration token__
-Used to make Gitlab CI aware of available runners.
+Used to make GitLab CI aware of available runners.
POST /ci/runners/register
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index a698fbc8184..4b1788a9af0 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -1,6 +1,6 @@
# Using Docker Build
-GitLab CI can allows you to use Docker Engine to build and test docker-based projects.
+GitLab CI allows you to use Docker Engine to build and test docker-based projects.
**This also allows to you to use `docker-compose` and other docker-enabled tools.**
@@ -35,7 +35,7 @@ GitLab Runner then executes build scripts as `gitlab-runner` user.
```bash
$ sudo gitlab-runner register -n \
- --url http://gitlab.com/ci \
+ --url https://gitlab.com/ci \
--token RUNNER_TOKEN \
--executor shell
--description "My Runner"
@@ -84,7 +84,7 @@ In order to do that follow the steps:
```bash
$ sudo gitlab-runner register -n \
- --url http://gitlab.com/ci \
+ --url https://gitlab.com/ci \
--token RUNNER_TOKEN \
--executor docker \
--description "My Docker Runner" \
@@ -108,5 +108,4 @@ In order to do that follow the steps:
```
1. However, by enabling `--docker-privileged` you are effectively disables all security mechanisms of containers and exposing your host to privilege escalation which can lead to container breakout.
-For more information you could be interested in checking out [Runtime privilege](https://docs.docker.com/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration).
-
+For more information, check out [Runtime privilege](https://docs.docker.com/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration). \ No newline at end of file
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 191e3a8144d..1feae62b1c7 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -60,11 +60,11 @@ This is image that have fully preconfigured `wordpress` and have `MySQL` server
```
Next time when you run your application the `tutum/wordpress` will be started
-and you will have access to it from your build container under hostname: `tutum_wordpress`.
+and you will have access to it from your build container under hostname: `tutum__wordpress`.
Alias hostname for the service is made from the image name:
1. Everything after `:` is stripped,
-2. '/' is replaced to `_`.
+2. '/' is replaced with `__`.
### Configuring services
Many services accept environment variables, which allow you to easily change database names or set account names depending on the environment.
@@ -90,7 +90,7 @@ you need to set MYSQL_ALLOW_EMPTY_PASSWORD.
- mysql
variables:
- MYSQL_ALLOW_EMPTY_PASSWORD: yes
+ MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
```
For other possible configuration variables check the
@@ -190,7 +190,7 @@ This will create two service containers (MySQL and PostgreSQL).
1. Create a build container and execute script in its context:
```
-$ cat build_script | docker run -n build -i -l mysql:service-mysql -l postgres:service-postgres ruby:2.1 /bin/bash
+$ docker run --name build -i --link=service-mysql:mysql --link=service-postgres:postgres ruby:2.1 /bin/bash < build_script
```
This will create build container that has two service containers linked.
The build_script is piped using STDIN to bash interpreter which executes the build script in container.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index e0b9fa0e25d..1cf41aea391 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,5 +1,5 @@
# Build script examples
-+ [Test and deploy Ruby Application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
-+ [Test and deploy Python Application to Heroku](test-and-deploy-python-application-to-heroku.md)
-+ [Test Clojure applications](examples/test-clojure-application.md)
++ [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
++ [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
++ [Test a Clojure application](test-clojure-application.md)
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
index 036b03dd6b9..a236da53fe9 100644
--- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -1,7 +1,7 @@
## Test and Deploy a python application
This example will guide you how to run tests in your Python application and deploy it automatically as Heroku application.
-You can checkout the example [source](https://gitlab.com/ayufan/python-getting-started) and check [CI status](https://ci.gitlab.com/projects/4080).
+You can checkout the example [source](https://gitlab.com/ayufan/python-getting-started) and check [CI status](https://gitlab.com/ayufan/python-getting-started/builds?scope=all).
### Configure project
This is what the `.gitlab-ci.yml` file looks like for this project:
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index d2a872f1934..e52e1547461 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -1,7 +1,7 @@
## Test and Deploy a ruby application
This example will guide you how to run tests in your Ruby application and deploy it automatiacally as Heroku application.
-You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://ci.gitlab.com/projects/4050).
+You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://gitlab.com/ayufan/ruby-getting-started/builds?scope=all).
### Configure project
This is what the `.gitlab-ci.yml` file looks like for this project:
@@ -64,4 +64,4 @@ gitlab-ci-multi-runner register \
With the command above, you create a runner that uses [ruby:2.1](https://registry.hub.docker.com/u/library/ruby/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database.
-To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password. \ No newline at end of file
+To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password.
diff --git a/doc/ci/examples/test-clojure-application.md b/doc/ci/examples/test-clojure-application.md
index eaee94a10f1..56b746ce025 100644
--- a/doc/ci/examples/test-clojure-application.md
+++ b/doc/ci/examples/test-clojure-application.md
@@ -1,8 +1,8 @@
-## Test Clojure applications
+## Test a Clojure application
This example will guide you how to run tests in your Clojure application.
-You can checkout the example [source](https://gitlab.com/dzaporozhets/clojure-web-application) and check [CI status](https://ci.gitlab.com/projects/6306).
+You can checkout the example [source](https://gitlab.com/dzaporozhets/clojure-web-application) and check [CI status](https://gitlab.com/dzaporozhets/clojure-web-application/builds?scope=all).
### Configure project
diff --git a/doc/ci/img/builds_tab.png b/doc/ci/img/builds_tab.png
new file mode 100644
index 00000000000..d088b8b329d
--- /dev/null
+++ b/doc/ci/img/builds_tab.png
Binary files differ
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index a87a1f806fc..a9b36139de9 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -1,44 +1,62 @@
# Quick Start
-To start building projects with GitLab CI a few steps needs to be done.
+Starting from version 8.0, GitLab Continuous Integration (CI) is fully
+integrated into GitLab itself and is enabled by default on all projects.
-## 1. Install GitLab and CI
+This guide assumes that you:
-First you need to have a working GitLab and GitLab CI instance.
+- have a working GitLab instance of version 8.0 or higher or are using
+ [GitLab.com](https://gitlab.com/users/sign_in)
+- have a project in GitLab that you would like to use CI for
-You can omit this step if you use [GitLab.com](http://GitLab.com/).
+In brief, the steps needed to have a working CI can be summed up to:
-## 2. Create repository on GitLab
+1. Create a new project
+1. Add `.gitlab-ci.yml` to the git repository and push to GitLab
+1. Configure a Runner
-Once you login on your GitLab add a new repository where you will store your source code.
-Push your application to that repository.
+From there on, on every push to your git repository the build will be
+automagically started by the Runner and will appear under the project's
+`/builds` page.
-## 3. Add project to CI
+Now, let's break it down to pieces and work on solving the GitLab CI puzzle.
-The next part is to login to GitLab CI.
-Point your browser to the URL you have set GitLab or use [gitlab.com/ci](http://gitlab.com/ci/).
+## Creating a `.gitlab-ci.yml` file
-On the first screen you will see a list of GitLab's projects that you have access to:
+Before you create `.gitlab-ci.yml` let's first explain in brief what this is
+all about.
-![Projects](projects.png)
+### What is `.gitlab-ci.yml`
-Click **Add Project to CI**.
-This will create project in CI and authorize GitLab CI to fetch sources from GitLab.
+The `.gitlab-ci.yml` file is where you configure what CI does with your project.
+It lives in the root of your repository.
-> GitLab CI creates unique token that is used to configure GitLab CI service in GitLab.
-> This token allows to access GitLab's repository and configures GitLab to trigger GitLab CI webhook on **Push events** and **Tag push events**.
-> You can see that token by going to Project's Settings > Services > GitLab CI.
-> You will see there token, the same token is assigned in GitLab CI settings of project.
+On any push to your repository, GitLab will look for the `.gitlab-ci.yml`
+file and start builds on _Runners_ according to the contents of the file,
+for that commit.
-## 4. Create project's configuration - .gitlab-ci.yml
+Because `.gitlab-ci.yml` is in the repository, it is version controlled,
+old versions still build succesfully, forks can easily make use of CI,
+branches can have separate builds and you have a single source of truth for CI.
+You can read more about the reasons why we are using `.gitlab-ci.yml`
+[in our blog about it][blog-ci].
-The next: You have to define how your project will be built.
-GitLab CI uses [YAML](https://en.wikipedia.org/wiki/YAML) file to store build configuration.
-You need to create `.gitlab-ci.yml` in root directory of your repository:
+**Note:** `.gitlab-ci.yml` is a [YAML](https://en.wikipedia.org/wiki/YAML) file
+so you have to pay extra attention to the identation. Always use spaces, not
+tabs.
+
+### Creating a simple `.gitlab-ci.yml` file
+
+You need to create a file named `.gitlab-ci.yml` in the root directory of your
+repository. Below is an example for a Ruby on Rails project.
```yaml
before_script:
- - bundle install
+ - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
+ - ruby -v
+ - which ruby
+ - gem install bundler --no-ri --no-rdoc
+ - bundle install --jobs $(nproc) "${FLAGS[@]}"
rspec:
script:
@@ -49,71 +67,131 @@ rubocop:
- bundle exec rubocop
```
-This is the simplest possible build configuration that will work for most Ruby applications:
-1. Define two jobs `rspec` and `rubocop` with two different commands to be executed.
-1. Before every job execute commands defined by `before_script`.
+This is the simplest possible build configuration that will work for most Ruby
+applications:
+
+1. Define two jobs `rspec` and `rubocop` (the names are arbitrary) with
+ different commands to be executed.
+1. Before every job, the commands defined by `before_script` are executed.
-The `.gitlab-ci.yml` defines set of jobs with constrains how and when they should be run.
-The jobs are defined as top-level elements with name and always have to contain the `script`.
-Jobs are used to create builds, which are then picked by [runners](../runners/README.md) and executed within environment of the runner.
-What is important that each job is run independently from each other.
+The `.gitlab-ci.yml` file defines sets of jobs with constraints of how and when
+they should be run. The jobs are defined as top-level elements with a name (in
+our case `rspec` and `rubocop`) and always have to contain the `script` keyword.
+Jobs are used to create builds, which are then picked by
+[Runners](../runners/README.md) and executed within the environment of the Runner.
-For more information and complete `.gitlab-ci.yml` syntax, please check the [Configuring project (.gitlab-ci.yml)](../yaml/README.md).
+What is important is that each job is run independently from each other.
-## 5. Add file and push .gitlab-ci.yml to repository
+If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
+Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
+the link under **Settings > CI settings** in your project.
-Once you created `.gitlab-ci.yml` you should add it to git repository and push it to GitLab.
+For more information and a complete `.gitlab-ci.yml` syntax, please check
+[the documentation on .gitlab-ci.yml](../yaml/README.md).
+
+### Push `.gitlab-ci.yml` to GitLab
+
+Once you've created `.gitlab-ci.yml`, you should add it to your git repository
+and push it to GitLab.
```bash
git add .gitlab-ci.yml
-git commit
+git commit -m "Add .gitlab-ci.yml"
git push origin master
```
-If you refresh the project's page on GitLab CI you will notice a one new commit:
+Now if you go to the **Builds** page you will see that the builds are pending.
+
+You can also go to the **Commits** page and notice the little clock icon next
+to the commit SHA.
+
+![New commit pending](img/new_commit.png)
+
+Clicking on the clock icon you will be directed to the builds page for that
+specific commit.
+
+![Single commit builds page](img/single_commit_status_pending.png)
+
+Notice that there are two jobs pending which are named after what we wrote in
+`.gitlab-ci.yml`. The red triangle indicates that there is no Runner configured
+yet for these builds.
+
+The next step is to configure a Runner so that it picks the pending jobs.
+
+## Configuring a Runner
+
+In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`.
+A Runner can be a virtual machine, a VPS, a bare-metal machine, a docker
+container or even a cluster of containers. GitLab and the Runners communicate
+through an API, so the only needed requirement is that the machine on which the
+Runner is configured to has Internet access.
+
+A Runner can be specific to a certain project or serve multiple projects in
+GitLab. If it serves all projects it's called a _Shared Runner_.
+
+Find more information about different Runners in the
+[Runners](../runners/README.md) documentation.
+
+You can find whether any Runners are assigned to your project by going to
+**Settings > Runners**. Setting up a Runner is easy and straightforward. The
+official Runner supported by GitLab is written in Go and can be found at
+<https://gitlab.com/gitlab-org/gitlab-ci-multi-runner>.
+
+In order to have a functional Runner you need to follow two steps:
+
+1. [Install it][runner-install]
+2. [Configure it](../runners/README.md#registering-a-specific-runner)
+
+Follow the links above to set up your own Runner or use a Shared Runner as
+described in the next section.
+
+For other types of unofficial Runners written in other languages, see the
+[instructions for the various GitLab Runners](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
+
+Once the Runner has been set up, you should see it on the Runners page of your
+project, following **Settings > Runners**.
-![](new_commit.png)
+![Activated runners](img/runners_activated.png)
-However the commit has status **pending** which means that commit was not yet picked by runner.
+### Shared Runners
-## 6. Configure runner
+If you use [GitLab.com](https://gitlab.com/) you can use **Shared Runners**
+provided by GitLab Inc.
-In GitLab CI, Runners run your builds.
-A runner is a machine (can be virtual, bare-metal or VPS) that picks up builds through the coordinator API of GitLab CI.
+These are special virtual machines that run on GitLab's infrastructure and can
+build any project.
-A runner can be specific to a certain project or serve any project in GitLab CI.
-A runner that serves all projects is called a shared runner.
-More information about different runner types can be found in [Configuring runner](../runners/README.md).
+To enable **Shared Runners** you have to go to your project's
+**Settings > Runners** and click **Enable shared runners**.
-To check if you have runners assigned to your project go to **Runners**. You will find there information how to setup project specific runner:
+[Read more on Shared Runners](../runners/README.md).
-1. Install GitLab Runner software. Checkout the [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner) section to install it.
-1. Specify following URL during runner setup: https://gitlab.com/ci/
-1. Use the following registration token during setup: TOKEN
+## Seeing the status of your build
-If you do it correctly your runner should be shown under **Runners activated for this project**:
+After configuring the Runner succesfully, you should see the status of your
+last commit change from _pending_ to either _running_, _success_ or _failed_.
-![](runners_activated.png)
+You can view all builds, by going to the **Builds** page in your project.
-### Shared runners
+![Commit status](img/builds_status.png)
-If you use [gitlab.com/ci](http://gitlab.com/ci/) you can use **Shared runners** provided by GitLab Inc.
-These are special virtual machines that are run on GitLab's infrastructure that can build any project.
-To enable **Shared runners** you have to go to **Runners** and click **Enable shared runners** for this project.
+By clicking on a Build ID, you will be able to see the log of that build.
+This is important to diagnose why a build failed or acted differently than
+you expected.
-## 7. Check status of commit
+![Build log](img/build_log.png)
-If everything went OK and you go to commit, the status of the commit should change from **pending** to either **running**, **success** or **failed**.
+You are also able to view the status of any commit in the various pages in
+GitLab, such as **Commits** and **Merge Requests**.
-![](commit_status.png)
+## Next steps
-You can click **Build ID** to view build log for specific job.
+Awesome! You started using CI in GitLab!
-## 8. Congratulations!
+Next you can look into doing more with the CI. Many people are using GitLab
+to package, containerize, test and deploy software.
-You managed to build your first project using GitLab CI.
-You may need to tune your `.gitlab-ci.yml` file to implement build plan for your project.
-A few examples how it can be done you can find on [Examples](../examples/README.md) page.
+Visit our various languages examples at <https://gitlab.com/groups/gitlab-examples>.
-GitLab CI also offers **the Lint** tool to verify validity of your `.gitlab-ci.yml` which can be useful to troubleshoot potential problems.
-The Lint is available from project's settings or by adding `/lint` to GitLab CI url.
+[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation
+[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
diff --git a/doc/ci/quick_start/build_status.png b/doc/ci/quick_start/build_status.png
deleted file mode 100644
index 333259e6acd..00000000000
--- a/doc/ci/quick_start/build_status.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/quick_start/commit_status.png b/doc/ci/quick_start/commit_status.png
deleted file mode 100644
index 725b79e6f91..00000000000
--- a/doc/ci/quick_start/commit_status.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/quick_start/img/build_log.png b/doc/ci/quick_start/img/build_log.png
new file mode 100644
index 00000000000..89e6cd40cb6
--- /dev/null
+++ b/doc/ci/quick_start/img/build_log.png
Binary files differ
diff --git a/doc/ci/quick_start/img/builds_status.png b/doc/ci/quick_start/img/builds_status.png
new file mode 100644
index 00000000000..b8e6c2a361a
--- /dev/null
+++ b/doc/ci/quick_start/img/builds_status.png
Binary files differ
diff --git a/doc/ci/quick_start/img/new_commit.png b/doc/ci/quick_start/img/new_commit.png
new file mode 100644
index 00000000000..3d3c9d5c0bd
--- /dev/null
+++ b/doc/ci/quick_start/img/new_commit.png
Binary files differ
diff --git a/doc/ci/quick_start/img/runners_activated.png b/doc/ci/quick_start/img/runners_activated.png
new file mode 100644
index 00000000000..eafcfd6ecd5
--- /dev/null
+++ b/doc/ci/quick_start/img/runners_activated.png
Binary files differ
diff --git a/doc/ci/quick_start/img/single_commit_status_pending.png b/doc/ci/quick_start/img/single_commit_status_pending.png
new file mode 100644
index 00000000000..23b3bb5acfc
--- /dev/null
+++ b/doc/ci/quick_start/img/single_commit_status_pending.png
Binary files differ
diff --git a/doc/ci/quick_start/img/status_pending.png b/doc/ci/quick_start/img/status_pending.png
new file mode 100644
index 00000000000..a049ec2a5ba
--- /dev/null
+++ b/doc/ci/quick_start/img/status_pending.png
Binary files differ
diff --git a/doc/ci/quick_start/new_commit.png b/doc/ci/quick_start/new_commit.png
deleted file mode 100644
index 3839e893c17..00000000000
--- a/doc/ci/quick_start/new_commit.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/quick_start/projects.png b/doc/ci/quick_start/projects.png
deleted file mode 100644
index 0b3430a69db..00000000000
--- a/doc/ci/quick_start/projects.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/quick_start/runners.png b/doc/ci/quick_start/runners.png
deleted file mode 100644
index 25b4046bc00..00000000000
--- a/doc/ci/quick_start/runners.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/quick_start/runners_activated.png b/doc/ci/quick_start/runners_activated.png
deleted file mode 100644
index c934bd12f41..00000000000
--- a/doc/ci/quick_start/runners_activated.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 04c6bf1e3a3..022afb70042 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -15,21 +15,27 @@ The API_TOKEN will take the Secure Variable value: `SECURE`.
### Predefined variables (Environment Variables)
-| Variable | Description |
+| Variable | Runner | Description |
|-------------------------|-------------|
-| **CI** | Mark that build is executed in CI environment |
-| **GITLAB_CI** | Mark that build is executed in GitLab CI environment |
-| **CI_SERVER** | Mark that build is executed in CI environment |
-| **CI_SERVER_NAME** | CI server that is used to coordinate builds |
-| **CI_SERVER_VERSION** | Not yet defined |
-| **CI_SERVER_REVISION** | Not yet defined |
-| **CI_BUILD_REF** | The commit revision for which project is built |
-| **CI_BUILD_BEFORE_SHA** | The first commit that were included in push request |
-| **CI_BUILD_REF_NAME** | The branch or tag name for which project is built |
-| **CI_BUILD_ID** | The unique id of the current build that GitLab CI uses internally |
-| **CI_BUILD_REPO** | The URL to clone the Git repository |
-| **CI_PROJECT_ID** | The unique id of the current project that GitLab CI uses internally |
-| **CI_PROJECT_DIR** | The full path where the repository is cloned and where the build is ran |
+| **CI** | 0.4 | Mark that build is executed in CI environment |
+| **GITLAB_CI** | all | Mark that build is executed in GitLab CI environment |
+| **CI_SERVER** | all | Mark that build is executed in CI environment |
+| **CI_SERVER_NAME** | all | CI server that is used to coordinate builds |
+| **CI_SERVER_VERSION** | all | Not yet defined |
+| **CI_SERVER_REVISION** | all | Not yet defined |
+| **CI_BUILD_REF** | all | The commit revision for which project is built |
+| **CI_BUILD_TAG** | 0.5 | The commit tag name. Present only when building tags. |
+| **CI_BUILD_NAME** | 0.5 | The name of the build as defined in `.gitlab-ci.yml` |
+| **CI_BUILD_STAGE** | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
+| **CI_BUILD_BEFORE_SHA** | all | The first commit that were included in push request |
+| **CI_BUILD_REF_NAME** | all | The branch or tag name for which project is built |
+| **CI_BUILD_ID** | all | The unique id of the current build that GitLab CI uses internally |
+| **CI_BUILD_REPO** | all | The URL to clone the Git repository |
+| **CI_BUILD_TRIGGERED** | 0.5 | The flag to indicate that build was triggered |
+| **CI_PROJECT_ID** | all | The unique id of the current project that GitLab CI uses internally |
+| **CI_PROJECT_DIR** | all | The full path where the repository is cloned and where the build is ran |
+
+**Some of the variables are only available when using runner with at least defined version.**
Example values:
@@ -39,6 +45,10 @@ export CI_BUILD_ID="50"
export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a"
export CI_BUILD_REF_NAME="master"
export CI_BUILD_REPO="https://gitlab.com/gitlab-org/gitlab-ce.git"
+export CI_BUILD_TAG="1.0.0"
+export CI_BUILD_NAME="spec:other"
+export CI_BUILD_STAGE="test"
+export CI_BUILD_TRIGGERED="true"
export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
export CI_PROJECT_ID="34"
export CI_SERVER="yes"
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 4caeccacb7f..3dbf1afc7a9 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -56,6 +56,7 @@ There are a few `keywords` that can't be used as job names:
| types | optional | Alias for `stages` |
| before_script | optional | Define commands prepended for each job's script |
| variables | optional | Define build variables |
+| cache | optional | Define list of files that should be cached between subsequent runs |
### image and services
This allows to specify a custom Docker image and a list of services that can be used for time of the build.
@@ -110,6 +111,19 @@ These variables can be later used in all executed commands and scripts.
The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them.
+### cache
+`cache` is used to specify list of files and directories which should be cached between builds.
+
+**The global setting allows to specify default cached files for all jobs.**
+
+To cache all git untracked files and files in `binaries`:
+```
+cache:
+ untracked: true
+ paths:
+ - binaries/
+```
+
## Jobs
`.gitlab-ci.yml` allows you to specify an unlimited number of jobs.
Each job has to have a unique `job_name`, which is not one of the keywords mentioned above.
@@ -140,6 +154,9 @@ job_name:
| except | optional | Defines a list of git refs for which build is not created |
| tags | optional | Defines a list of tags which are used to select runner |
| allow_failure | optional | Allow build to fail. Failed build doesn't contribute to commit status |
+| when | optional | Define when to run build. Can be `on_success`, `on_failure` or `always` |
+| artifacts | optional | Define list build artifacts |
+| cache | optional | Define list of files that should be cached between subsequent runs |
### script
`script` is a shell script which is executed by runner. The shell script is prepended with `before_script`.
@@ -168,7 +185,7 @@ This are two parameters that allow for setting a refs policy to limit when jobs
There are a few rules that apply to usage of refs policy:
-1. `only` and `except` are exclusive. If both `only` and `except` are defined in job specification only `only` is taken into account.
+1. `only` and `except` are inclusive. If both `only` and `except` are defined in job specification the ref is filtered by `only` and `except`.
1. `only` and `except` allow for using the regexp expressions.
1. `only` and `except` allow for using special keywords: `branches` and `tags`.
These names can be used for example to exclude all tags and all branches.
@@ -181,6 +198,18 @@ job:
- branches # use special keyword
```
+1. `only` and `except` allow for specify repository path to filter jobs for forks.
+The repository path can be used to have jobs executed only for parent repository.
+
+```yaml
+job:
+ only:
+ - branches@gitlab-org/gitlab-ce
+ except:
+ - master@gitlab-org/gitlab-ce
+```
+The above will run `job` for all branches on `gitlab-org/gitlab-ce`, except master .
+
### tags
`tags` is used to select specific runners from the list of all runners that are allowed to run this project.
@@ -196,9 +225,130 @@ job:
The above specification will make sure that `job` is built by a runner that have `ruby` AND `postgres` tags defined.
+### when
+`when` is used to implement jobs that are run in case of failure or despite the failure.
+
+`when` can be set to one of the following values:
+
+1. `on_success` - execute build only when all builds from prior stages succeeded. This is the default.
+1. `on_failure` - execute build only when at least one build from prior stages failed.
+1. `always` - execute build despite the status of builds from prior stages.
+
+```
+stages:
+- build
+- cleanup_build
+- test
+- deploy
+- cleanup
+
+build:
+ stage: build
+ script:
+ - make build
+
+cleanup_build:
+ stage: cleanup_build
+ script:
+ - cleanup build when failed
+ when: on_failure
+
+test:
+ stage: test
+ script:
+ - make test
+
+deploy:
+ stage: deploy
+ script:
+ - make deploy
+
+cleanup:
+ stage: cleanup
+ script:
+ - cleanup after builds
+ when: always
+```
+
+The above script will:
+1. Execute `cleanup_build` only when the `build` failed,
+2. Always execute `cleanup` as the last step in pipeline.
+
+### artifacts
+`artifacts` is used to specify list of files and directories which should be attached to build after success.
+
+1. Send all files in `binaries` and `.config`:
+
+ artifacts:
+ paths:
+ - binaries/
+ - .config
+
+2. Send all git untracked files:
+
+ artifacts:
+ untracked: true
+
+3. Send all git untracked files and files in `binaries`:
+
+ artifacts:
+ untracked: true
+ paths:
+ - binaries/
+
+The artifacts will be send after the build success to GitLab and will be accessible in GitLab interface to download.
+
+This feature requires GitLab Runner v0.7.0 or higher.
+
+### cache
+`cache` is used to specify list of files and directories which should be cached between builds.
+
+1. Cache all files in `binaries` and `.config`:
+
+ rspec:
+ script: test
+ cache:
+ paths:
+ - binaries/
+ - .config
+
+2. Cache all git untracked files:
+
+ rspec:
+ script: test
+ cache:
+ untracked: true
+
+3. Cache all git untracked files and files in `binaries`:
+
+ rspec:
+ script: test
+ cache:
+ untracked: true
+ paths:
+ - binaries/
+
+4. Locally defined cache overwrites globally defined options. This will cache only `binaries/`:
+
+ cache:
+ paths:
+ - my/files
+
+ rspec:
+ script: test
+ cache:
+ paths:
+ - binaries/
+
+The cache is provided on best effort basis, so don't expect that cache will be present.
+For implementation details please check GitLab Runner.
+
+This feature requires GitLab Runner v0.7.0 or higher.
+
+
## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint.
You can find the link to the Lint in the project's settings page or use short url `/lint`.
## Skipping builds
-There is one more way to skip all builds, if your commit message contains tag [ci skip]. In this case, commit will be created but builds will be skipped \ No newline at end of file
+There is one more way to skip all builds, if your commit message contains tag [ci skip]. In this case, commit will be created but builds will be skipped
diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md
index 54c1780c3ab..bd2c242afc2 100644
--- a/doc/customization/libravatar.md
+++ b/doc/customization/libravatar.md
@@ -2,7 +2,7 @@
GitLab by default supports [Gravatar](https://gravatar.com) avatar service.
Libravatar is a service which delivers your avatar (profile picture) to other websites and their API is
-[heavily based on gravatar](http://wiki.libravatar.org/api/).
+[heavily based on gravatar](https://wiki.libravatar.org/api/).
This means that it is not complicated to switch to Libravatar avatar service or even self hosted Libravatar server.
@@ -31,7 +31,7 @@ the configuration options as follows:
## Self-hosted
-If you are [running your own libravatar service](http://wiki.libravatar.org/running_your_own/) the URL will be different in the configuration
+If you are [running your own libravatar service](https://wiki.libravatar.org/running_your_own/) the URL will be different in the configuration
but the important part is to provide the same placeholders so GitLab can parse the URL correctly.
For example, you host a service on `http://libravatar.example.com` the `plain_url` you need to supply in `gitlab.yml` is
@@ -63,7 +63,7 @@ Run `sudo gitlab-ctl reconfigure` for changes to take effect.
## Default URL for missing images
-[Libravatar supports different sets](http://wiki.libravatar.org/api/) of `missing images` for emails not found on the Libravatar service.
+[Libravatar supports different sets](https://wiki.libravatar.org/api/) of `missing images` for emails not found on the Libravatar service.
In order to use a different set other than `identicon`, replace `&d=identicon` portion of the URL with another supported set.
For example, you can use `retro` set in which case the URL would look like: `plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=retro"`
diff --git a/doc/customization/welcome_message.md b/doc/customization/welcome_message.md
index 6c141d1fb7a..e993230bb88 100644
--- a/doc/customization/welcome_message.md
+++ b/doc/customization/welcome_message.md
@@ -8,31 +8,5 @@ It is possible to add a markdown-formatted welcome message to your GitLab
sign-in page. Users of GitLab Enterprise Edition should use the [branded login
page feature](/ee/customization/branded_login_page.html) instead.
-## Omnibus-gitlab example
-
-In `/etc/gitlab/gitlab.rb`:
-
-```ruby
-gitlab_rails['extra_sign_in_text'] = <<'EOS'
-# ACME GitLab
-Welcome to the [ACME](http://www.example.com) GitLab server!
-EOS
-```
-
-Run `sudo gitlab-ctl reconfigure` for changes to take effect.
-
-## Installation from source
-
-In `/home/git/gitlab/config/gitlab.yml`:
-
-```yaml
-# snip
-production:
- # snip
- extra:
- sign_in_text: |
- # ACME GitLab
- Welcome to the [ACME](http://www.example.com) GitLab server!
-```
-
-Run `sudo service gitlab reload` for the change to take effect.
+The welcome message (extra_sign_in_text) can now be set/changed in the Admin UI.
+Admin area > Settings \ No newline at end of file
diff --git a/doc/development/README.md b/doc/development/README.md
index 6bc8e1888db..d5bf166ad32 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -8,3 +8,4 @@
- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements
- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
- [How to dump production data to staging](dump_db.md)
+- [Benchmarking](benchmarking.md)
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index c00d290371e..6101a71a8de 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -146,7 +146,7 @@ nginx
Apache httpd
-- [Explanation of Apache logs](http://httpd.apache.org/docs/2.2/logs.html).
+- [Explanation of Apache logs](https://httpd.apache.org/docs/2.2/logs.html).
- `/var/log/apache2/` contains error and output logs (on Ubuntu).
- `/var/log/httpd/` contains error and output logs (on RHEL).
diff --git a/doc/development/benchmarking.md b/doc/development/benchmarking.md
new file mode 100644
index 00000000000..88e18ee95f9
--- /dev/null
+++ b/doc/development/benchmarking.md
@@ -0,0 +1,69 @@
+# Benchmarking
+
+GitLab CE comes with a set of benchmarks that are executed for every build. This
+makes it easier to measure performance of certain components over time.
+
+Benchmarks are written as RSpec tests using a few extra helpers. To write a
+benchmark, first tag the top-level `describe`:
+
+```ruby
+describe MaruTheCat, benchmark: true do
+
+end
+```
+
+This ensures the benchmark is executed separately from other test collections.
+It also exposes the various RSpec matchers used for writing benchmarks to the
+test group.
+
+Next, lets write the actual benchmark:
+
+```ruby
+describe MaruTheCat, benchmark: true do
+ let(:maru) { MaruTheChat.new }
+
+ describe '#jump_in_box' do
+ benchmark_subject { maru.jump_in_box }
+
+ it { is_expected.to iterate_per_second(9000) }
+ end
+end
+```
+
+Here `benchmark_subject` is a small wrapper around RSpec's `subject` method that
+makes it easier to specify the subject of a benchmark. Using RSpec's regular
+`subject` would require us to write the following instead:
+
+```ruby
+subject { -> { maru.jump_in_box } }
+```
+
+The `iterate_per_second` matcher defines the amount of times per second a
+subject should be executed. The higher the amount of iterations the better.
+
+By default the allowed standard deviation is a maximum of 30%. This can be
+adjusted by chaining the `with_maximum_stddev` on the `iterate_per_second`
+matcher:
+
+```ruby
+it { is_expected.to iterate_per_second(9000).with_maximum_stddev(50) }
+```
+
+This can be useful if the code in question depends on external resources of
+which the performance can vary a lot (e.g. physical HDDs, network calls, etc).
+However, in most cases 30% should be enough so only change this when really
+needed.
+
+## Benchmarks Location
+
+Benchmarks should be stored in `spec/benchmarks` and should follow the regular
+Rails specs structure. That is, model benchmarks go in `spec/benchmark/models`,
+benchmarks for code in the `lib` directory go in `spec/benchmarks/lib`, etc.
+
+## Underlying Technology
+
+The benchmark setup uses [benchmark-ips][benchmark-ips] which takes care of the
+heavy lifting such as warming up code, calculating iterations, standard
+deviation, etc.
+
+[benchmark-ips]: https://github.com/evanphx/benchmark-ips
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
new file mode 100644
index 00000000000..e244ad4e881
--- /dev/null
+++ b/doc/development/profiling.md
@@ -0,0 +1,27 @@
+# Profiling
+
+To make it easier to track down performance problems GitLab comes with a set of
+profiling tools, some of these are available by default while others need to be
+explicitly enabled.
+
+## Sherlock
+
+Sherlock is a custom profiling tool built into GitLab. Sherlock is _only_
+available when running GitLab in development mode _and_ when setting the
+environment variable `ENABLE_SHERLOCK` to a non empty value. For example:
+
+ ENABLE_SHERLOCK=1 bundle exec rails s
+
+Recorded transactions can be found by navigating to `/sherlock/transactions`.
+
+## Bullet
+
+Bullet is a Gem that can be used to track down N+1 query problems. Because
+Bullet adds quite a bit of logging noise it's disabled by default. To enable
+Bullet, set the environment variable `ENABLE_BULLET` to a non-empty value before
+starting GitLab. For example:
+
+ ENABLE_BULLET=true bundle exec rails s
+
+Bullet will log query problems to both the Rails log as well as the Chrome
+console.
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index a4a980cf0e0..9f3fd69fc4e 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -9,7 +9,7 @@ bundle exec rake setup
```
The `setup` task is a alias for `gitlab:setup`.
-This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and fianlly it calls `db:seed_fu` to seed the database.
+This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database.
Note: `db:setup` calls `db:seed` but this does nothing.
## Run tests
diff --git a/doc/development/shared_files.md b/doc/development/shared_files.md
new file mode 100644
index 00000000000..fcd905b54a4
--- /dev/null
+++ b/doc/development/shared_files.md
@@ -0,0 +1,33 @@
+# Shared files
+
+Historically, GitLab has been storing shared files in many different
+directories: `public/uploads`, `builds`, `tmp/repositories`, `tmp/rebase` (EE),
+etc. Having so many shared directories makes it difficult to deploy GitLab on
+shared storage (e.g. NFS). Working towards GitLab 9.0 we are consolidating
+these different directories under the `shared` directory.
+
+This means that if GitLab will start storing puppies in some future version
+then we should put them in `shared/puppies`. Temporary puppy files should be
+stored in `shared/tmp`.
+
+In the GitLab application code you can get the full path to the `shared`
+directory with `Gitlab.config.shared.path`.
+
+## What is not a 'shared file'
+
+Files that belong to only one process, or on only one server, should not go in
+`shared`. Examples include PID files and sockets.
+
+## Temporary files and shared storage
+
+Sometimes you create a temporary file on disk with the intention of it becoming
+'official'. For example you might be first streaming an upload from a user to
+disk in a temporary file so you can perform some checks on it. When the checks
+pass, you make the file official. In scenarios like this please follow these
+rules:
+
+- Store the temporary file under `shared/tmp`, i.e. on the same filesystem you
+ want the official file to be on.
+- Use move/rename operations when operating on the file instead of copy
+ operations where possible, because renaming a file is much faster than
+ copying it.
diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md
index 2d1d0fb4154..65cdd74bdb6 100644
--- a/doc/development/shell_commands.md
+++ b/doc/development/shell_commands.md
@@ -35,6 +35,16 @@ Gitlab::Popen.popen(%W(find /some/path -not -path /some/path -mmin +120 -delete)
This coding style could have prevented CVE-2013-4490.
+## Always use the configurable git binary path for git commands
+
+```ruby
+# Wrong
+system(*%W(git branch -d -- #{branch_name}))
+
+# Correct
+system(*%W(#{Gitlab.config.git.bin_path} branch -d -- #{branch_name}))
+```
+
## Bypass the shell by splitting commands into separate tokens
When we pass shell commands as a single string to Ruby, Ruby will let `/bin/sh` evaluate the entire string. Essentially, we are asking the shell to evaluate a one-line script. This creates a risk for shell injection attacks. It is better to split the shell command into tokens ourselves. Sometimes we use the scripting capabilities of the shell to change the working directory or set environment variables. All of this can also be achieved securely straight from Ruby
@@ -81,9 +91,9 @@ In the GitLab codebase, we avoid the option/argument ambiguity by _always_ using
```ruby
# Wrong
-system(*%W(git branch -d #{branch_name}))
+system(*%W(#{Gitlab.config.git.bin_path} branch -d #{branch_name}))
# Correct
-system(*%W(git branch -d -- #{branch_name}))
+system(*%W(#{Gitlab.config.git.bin_path} branch -d -- #{branch_name}))
```
This coding style could have prevented CVE-2013-4582.
@@ -94,9 +104,9 @@ Capturing the output of shell commands with backticks reads nicely, but you are
```ruby
# Wrong
-logs = `cd #{repo_dir} && git log`
+logs = `cd #{repo_dir} && #{Gitlab.config.git.bin_path} log`
# Correct
-logs, exit_status = Gitlab::Popen.popen(%W(git log), repo_dir)
+logs, exit_status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} log), repo_dir)
# Wrong
user = `whoami`
@@ -108,7 +118,7 @@ In other repositories, such as gitlab-shell you can also use `IO.popen`.
```ruby
# Safe IO.popen example
-logs = IO.popen(%W(git log), chdir: repo_dir) { |p| p.read }
+logs = IO.popen(%W(#{Gitlab.config.git.bin_path} log), chdir: repo_dir) { |p| p.read }
```
Note that unlike `Gitlab::Popen.popen`, `IO.popen` does not capture standard error.
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index b904c70e980..493e1d1b09c 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -23,3 +23,5 @@ Step-by-step guides on the basics of working with Git and GitLab.
* [Add an image](add-image.md)
* [Create a Merge Request](add-merge-request.md)
+
+* [Create an Issue](create-issue.md)
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
new file mode 100644
index 00000000000..87f078def04
--- /dev/null
+++ b/doc/gitlab-basics/create-issue.md
@@ -0,0 +1,27 @@
+# How to create an Issue in GitLab
+
+The Issue Tracker is a good place to add things that need to be improved or solved in a project.
+
+To create an Issue, sign in to GitLab.
+
+Go to the project where you'd like to create the Issue:
+
+![Select a project](basicsimages/select_project.png)
+
+Click on "Issues" on the left side of your screen:
+
+![Issues](basicsimages/issues.png)
+
+Click on the "+ new issue" button on the right side of your screen:
+
+![New issue](basicsimages/new_issue.png)
+
+Add a title and a description to your issue:
+
+![Issue title and description](basicsimages/issue_title.png)
+
+You may assign the Issue to a user, add a milestone and add labels (they are all optional). Then click on "submit new issue":
+
+![Submit new issue](basicsimages/submit_new_issue.png)
+
+Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://doc.gitlab.com/ce/customization/issue_closing.html).
diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md
index f7d4f3de68b..0f2665a3bf7 100644
--- a/doc/hooks/custom_hooks.md
+++ b/doc/hooks/custom_hooks.md
@@ -2,12 +2,12 @@
**Note: Custom git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
-Please explore webhooks as an option if you do not have filesystem access.**
+Please explore webhooks as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
Git natively supports hooks that are executed on different actions.
Examples of server-side git hooks include pre-receive, post-receive, and update.
See
-[Git SCM Server-Side Hooks](http://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks)
+[Git SCM Server-Side Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks)
for more information about each hook type.
As of gitlab-shell version 2.2.0 (which requires GitLab 7.5+), GitLab
diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md
index 01ab22321ed..86d205ba7a5 100644
--- a/doc/incoming_email/README.md
+++ b/doc/incoming_email/README.md
@@ -4,32 +4,75 @@ GitLab can be set up to allow users to comment on issues and merge requests by r
## Get a mailbox
-Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises.
+Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises.
-If you want to use Gmail with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
+If you want to use Gmail / Google Apps with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these instructions](./postfix.md).
## Set it up
-In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`.
-
### Omnibus package installations
-1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature, enter the email address including a placeholder for the `key` that references the item being replied to and fill in the details for your specific IMAP server and email account:
+1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature and fill in the details for your specific IMAP server and email account:
```ruby
+ # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
gitlab_rails['incoming_email_enabled'] = true
- gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
- gitlab_rails['incoming_email_host'] = "imap.gmail.com" # IMAP server host
- gitlab_rails['incoming_email_port'] = 993 # IMAP server port
- gitlab_rails['incoming_email_ssl'] = true # Whether the IMAP server uses SSL
- gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" # Email account username. Usually the full email address.
- gitlab_rails['incoming_email_password'] = "password" # Email account password
- gitlab_rails['incoming_email_mailbox_name'] = "inbox" # The name of the mailbox where incoming mail will end up. Usually "inbox".
+
+ # The email address including a placeholder for the key that references the item being replied to.
+ # The `%{key}` placeholder is added after the user part, before the `@`.
+ gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ gitlab_rails['incoming_email_email'] = "incoming"
+ # Email account password
+ gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+ # IMAP server host
+ gitlab_rails['incoming_email_host'] = "gitlab.example.com"
+ # IMAP server port
+ gitlab_rails['incoming_email_port'] = 143
+ # Whether the IMAP server uses SSL
+ gitlab_rails['incoming_email_ssl'] = false
+ # Whether the IMAP server uses StartTLS
+ gitlab_rails['incoming_email_start_tls'] = false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ gitlab_rails['incoming_email_mailbox_name'] = "inbox"
```
- As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `gitlab-incoming@gmail.com`.
+ ```ruby
+ # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
+ gitlab_rails['incoming_email_enabled'] = true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`.
+ gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com"
+ # Email account password
+ gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+ # IMAP server host
+ gitlab_rails['incoming_email_host'] = "imap.gmail.com"
+ # IMAP server port
+ gitlab_rails['incoming_email_port'] = 993
+ # Whether the IMAP server uses SSL
+ gitlab_rails['incoming_email_ssl'] = true
+ # Whether the IMAP server uses StartTLS
+ gitlab_rails['incoming_email_start_tls'] = false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+ ```
+
+ As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
1. Reconfigure GitLab for the changes to take effect:
@@ -53,155 +96,146 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`.
cd /home/git/gitlab
```
-1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `key` that references the item being replied to:
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account:
```sh
sudo editor config/gitlab.yml
```
```yaml
+ # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
incoming_email:
enabled: true
- address: "gitlab-incoming+%{key}@gmail.com"
- ```
-
- As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `gitlab-incoming@gmail.com`.
-2. Copy `config/mail_room.yml.example` to `config/mail_room.yml`:
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`.
+ address: "incoming+%{key}@gitlab.example.com"
- ```sh
- sudo cp config/mail_room.yml.example config/mail_room.yml
- ```
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "incoming"
+ # Email account password
+ password: "[REDACTED]"
-3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account:
+ # IMAP server host
+ host: "gitlab.example.com"
+ # IMAP server port
+ port: 143
+ # Whether the IMAP server uses SSL
+ ssl: false
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
- ```sh
- sudo editor config/mail_room.yml
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
```
```yaml
- :mailboxes:
- -
- # IMAP server host
- :host: "imap.gmail.com"
- # IMAP server port
- :port: 993
- # Whether the IMAP server uses SSL
- :ssl: true
- # Whether the IMAP server uses StartTLS
- :start_tls: false
- # Email account username. Usually the full email address.
- :email: "gitlab-incoming@gmail.com"
- # Email account password
- :password: "[REDACTED]"
- # The name of the mailbox where incoming mail will end up. Usually "inbox".
- :name: "inbox"
- # Always "sidekiq".
- :delivery_method: sidekiq
- # Always true.
- :delete_after_delivery: true
- :delivery_options:
- # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml.
- :redis_url: redis://localhost:6379
- # Always "resque:gitlab".
- :namespace: resque:gitlab
- # Always "incoming_email".
- :queue: incoming_email
- # Always "EmailReceiverWorker"
- :worker: EmailReceiverWorker
+ # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
+ incoming_email:
+ enabled: true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`.
+ address: "gitlab-incoming+%{key}@gmail.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "gitlab-incoming@gmail.com"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "imap.gmail.com"
+ # IMAP server port
+ port: 993
+ # Whether the IMAP server uses SSL
+ ssl: true
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
```
-5. Edit the init script configuration at `/etc/default/gitlab` to enable `mail_room`:
+ As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
+
+1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
```sh
sudo mkdir -p /etc/default
echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab
```
-6. Restart GitLab:
+1. Restart GitLab:
```sh
sudo service gitlab restart
```
-7. Verify that everything is configured correctly:
+1. Verify that everything is configured correctly:
```sh
sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production
```
-8. Reply by email should now be working.
+1. Reply by email should now be working.
### Development
1. Go to the GitLab installation directory.
-1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `key` that references the item being replied to:
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account:
```yaml
+ # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
incoming_email:
enabled: true
+
+ # The email address including a placeholder for the key that references the item being replied to.
+ # The `%{key}` placeholder is added after the user part, before the `@`.
address: "gitlab-incoming+%{key}@gmail.com"
- ```
- As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "gitlab-incoming@gmail.com"
+ # Email account password
+ password: "[REDACTED]"
-2. Copy `config/mail_room.yml.example` to `config/mail_room.yml`:
+ # IMAP server host
+ host: "imap.gmail.com"
+ # IMAP server port
+ port: 993
+ # Whether the IMAP server uses SSL
+ ssl: true
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
- ```sh
- sudo cp config/mail_room.yml.example config/mail_room.yml
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
```
-3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account:
-
- ```yaml
- :mailboxes:
- -
- # IMAP server host
- :host: "imap.gmail.com"
- # IMAP server port
- :port: 993
- # Whether the IMAP server uses SSL
- :ssl: true
- # Whether the IMAP server uses StartTLS
- :start_tls: false
- # Email account username. Usually the full email address.
- :email: "gitlab-incoming@gmail.com"
- # Email account password
- :password: "[REDACTED]"
- # The name of the mailbox where incoming mail will end up. Usually "inbox".
- :name: "inbox"
- # Always "sidekiq".
- :delivery_method: sidekiq
- # Always true.
- :delete_after_delivery: true
- :delivery_options:
- # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml.
- :redis_url: redis://localhost:6379
- # Always "resque:gitlab".
- :namespace: resque:gitlab
- # Always "incoming_email".
- :queue: incoming_email
- # Always "EmailReceiverWorker"
- :worker: EmailReceiverWorker
- ```
+ As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
-4. Uncomment the `mail_room` line in your `Procfile`:
+1. Uncomment the `mail_room` line in your `Procfile`:
```yaml
mail_room: bundle exec mail_room -q -c config/mail_room.yml
```
-6. Restart GitLab:
+1. Restart GitLab:
```sh
bundle exec foreman start
```
-7. Verify that everything is configured correctly:
+1. Verify that everything is configured correctly:
```sh
bundle exec rake gitlab:incoming_email:check RAILS_ENV=development
```
-8. Reply by email should now be working.
+1. Reply by email should now be working.
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index c565e90da2f..513ad69ec26 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -2,7 +2,7 @@
## Note
-We do not recommend using MySQL due to various issues. For example, case [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html) and [problems](http://bugs.mysql.com/bug.php?id=65830) that [suggested](http://bugs.mysql.com/bug.php?id=50909) [fixes](http://bugs.mysql.com/bug.php?id=65830) [have](http://bugs.mysql.com/bug.php?id=63164).
+We do not recommend using MySQL due to various issues. For example, case [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html) and [problems](https://bugs.mysql.com/bug.php?id=65830) that [suggested](https://bugs.mysql.com/bug.php?id=50909) [fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164).
## MySQL
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 039bb3c2561..618391e16d2 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -106,7 +106,7 @@ Then select 'Internet Site' and press enter to confirm the hostname.
## 2. Ruby
-The use of Ruby version managers such as [RVM](http://rvm.io/), [rbenv](https://github.com/sstephenson/rbenv) or [chruby](https://github.com/postmodern/chruby) with GitLab in production frequently leads to hard to diagnose problems. For example, GitLab Shell is called from OpenSSH and having a version manager can prevent pushing and pulling over SSH. Version managers are not supported and we strongly advise everyone to follow the instructions below to use a system Ruby.
+The use of Ruby version managers such as [RVM](https://rvm.io/), [rbenv](https://github.com/sstephenson/rbenv) or [chruby](https://github.com/postmodern/chruby) with GitLab in production frequently leads to hard to diagnose problems. For example, GitLab Shell is called from OpenSSH and having a version manager can prevent pushing and pulling over SSH. Version managers are not supported and we strongly advise everyone to follow the instructions below to use a system Ruby.
Remove the old Ruby 1.8 if present
@@ -115,8 +115,9 @@ Remove the old Ruby 1.8 if present
Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
- curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz
- cd ruby-2.1.6
+ curl -O --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.7.tar.gz
+ echo 'e2e195a4a58133e3ad33b955c829bb536fa3c075 ruby-2.1.7.tar.gz' | shasum -c - && tar xzf ruby-2.1.7.tar.gz
+ cd ruby-2.1.7
./configure --disable-install-rdoc
make
sudo make install
@@ -127,15 +128,17 @@ Install the Bundler Gem:
## 3. Go
-Since GitLab 8.0, Git HTTP requests are handled by gitlab-git-http-server.
-This is a small daemon written in Go.
-To install gitlab-git-http-server we need a Go compiler.
+Since GitLab 8.0, Git HTTP requests are handled by gitlab-workhorse (formerly
+gitlab-git-http-server). This is a small daemon written in Go. To install
+gitlab-workhorse we need a Go compiler. The instructions below assume you
+use 64-bit Linux. You can find downloads for other platforms at the [Go download
+page](https://golang.org/dl).
- curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz
- echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \
- sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz
+ curl -O --progress https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz
+ echo '46eecd290d8803887dec718c691cc243f2175fe0 go1.5.1.linux-amd64.tar.gz' | shasum -c - && \
+ sudo tar -C /usr/local -xzf go1.5.1.linux-amd64.tar.gz
sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
- rm go1.5.linux-amd64.tar.gz
+ rm go1.5.1.linux-amd64.tar.gz
## 4. System Users
@@ -207,9 +210,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-0-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-2-stable gitlab
-**Note:** You can change `8-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -242,6 +245,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
# Change the permissions of the directory where CI build traces are stored
sudo chmod -R u+rwX builds/
+ # Change the permissions of the directory where CI artifacts are stored
+ sudo chmod -R u+rwX shared/artifacts/
+
# Copy the example Unicorn config
sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb
@@ -249,8 +255,8 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
nproc
# Enable cluster mode if you expect to have a high load instance
- # Ex. change amount of workers to 3 for 2GB RAM server
# Set the number of workers to at least the number of cores
+ # Ex. change amount of workers to 3 for 2GB RAM server
sudo -u git -H editor config/unicorn.rb
# Copy the example Rack attack config
@@ -291,7 +297,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
### Install Gems
-**Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](http://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2.
+**Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](https://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2.
# For PostgreSQL (note, the option says "without ... mysql")
sudo -u git -H bundle install --deployment --without development test mysql aws kerberos
@@ -306,7 +312,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
GitLab Shell is an SSH access and repository management software developed specially for GitLab.
# Run the installation task for gitlab-shell (replace `REDIS_URL` if needed):
- sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.5] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production
+ sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production
# By default, the gitlab-shell config is generated from your main GitLab config.
# You can review (and modify) the gitlab-shell config as follows:
@@ -314,17 +320,22 @@ GitLab Shell is an SSH access and repository management software developed speci
**Note:** If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps.
-**Note:** Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in /etc/hosts ("127.0.0.1 hostname"). This might be necessary for example if you set up gitlab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)".
+**Note:** Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in /etc/hosts ("127.0.0.1 hostname"). This might be necessary for example if you set up GitLab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)".
-### Install gitlab-git-http-server
+### Install gitlab-workhorse
cd /home/git
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git
- cd gitlab-git-http-server
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
+ cd gitlab-workhorse
+ sudo -u git -H git checkout 0.4.2
sudo -u git -H make
### Initialize Database and Activate Advanced Features
+ # Go to GitLab installation folder
+
+ cd /home/git/gitlab
+
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production
# Type 'yes' to create the database tables.
@@ -341,11 +352,6 @@ The `secrets.yml` file stores encryption keys for sessions and secure variables.
Backup `secrets.yml` someplace safe, but don't store it in the same place as your database backups.
Otherwise your secrets are exposed if one of your backups is compromised.
-### Install schedules
-
- # Setup schedules
- sudo -u gitlab_ci -H bundle exec whenever -w RAILS_ENV=production
-
### Install Init Script
Download the init script (will be `/etc/init.d/gitlab`):
@@ -489,7 +495,7 @@ See the [omniauth integration document](../integration/omniauth.md)
### Build your projects
GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you.
-Checkout the [Gitlab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it
+Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it
### Custom Redis Connection
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index aa0d03b75bc..c0ccdd37458 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -109,8 +109,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o
- Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/))
- Safari 7+ (known problem: required fields in html5 do not work)
- Opera (Latest released version)
-- IE 10+
-
-### Common UI problems with IE
-
-If you experience UI issues with Internet Explorer, please make sure that you have the `Compatibility View` mode disabled. \ No newline at end of file
+- Internet Explorer (IE) 10+ but please make sure that you have the `Compatibility View` mode disabled. \ No newline at end of file
diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md
new file mode 100644
index 00000000000..bc1f1673086
--- /dev/null
+++ b/doc/integration/facebook.md
@@ -0,0 +1,97 @@
+# Facebook OAuth2 OmniAuth Provider
+
+To enable the Facebook OmniAuth provider you must register your application with Facebook. Facebook will generate an app ID and secret key for you to use.
+
+1. Sign in to the [Facebook Developer Platform](https://developers.facebook.com/).
+
+1. Choose "My Apps" &gt; "Add a New App"
+
+1. Select the type "Website"
+
+1. Enter a name for your app. This can be anything. Consider something like "&lt;Organization&gt;'s GitLab" or "&lt;Your Name&gt;'s GitLab" or
+something else descriptive.
+
+1. Choose "Create New Facebook App ID"
+
+1. Select a Category, for example "Productivity"
+
+1. Choose "Create App ID"
+
+1. Enter the address of your GitLab installation at the bottom of the package
+
+ ![Facebook Website URL](facebook_website_url.png)
+
+1. Choose "Next"
+
+1. Choose "Skip Quick Start" in the upper right corner
+
+1. Choose "Settings" in the menu on the left
+
+1. Fill in a contact email for your app
+
+ ![Facebook App Settings](facebook_app_settings.png)
+
+1. Choose "Save Changes"
+
+1. Choose "Status & Review" in the menu on the left
+
+1. Change the switch on the right from No to Yes
+
+1. Choose "Confirm" when prompted to make the app public
+
+1. Choose "Dashboard" in the menu on the left
+
+1. Choose "Show" next to the hidden "App Secret"
+
+1. You should now see an app key and app secret (see screenshot). Keep this page open as you continue configuration.
+
+ ![Facebook API Keys](facebook_api_keys.png)
+
+1. On your GitLab server, open the configuration file.
+
+ For omnibus package:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ cd /home/git/gitlab
+
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+
+1. Add the provider configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "facebook",
+ "app_id" => "YOUR_APP_ID",
+ "app_secret" => "YOUR_APP_SECRET"
+ }
+ ]
+ ```
+
+ For installations from source:
+
+ ```
+ - { name: 'facebook', app_id: 'YOUR_APP_ID',
+ app_secret: 'YOUR_APP_SECRET' }
+ ```
+
+1. Change 'YOUR_APP_ID' to the API key from Facebook page in step 10.
+
+1. Change 'YOUR_APP_SECRET' to the API secret from the Facebook page in step 10.
+
+1. Save the configuration file.
+
+1. Restart GitLab for the changes to take effect.
+
+On the sign in page there should now be a Facebook icon below the regular sign in form. Click the icon to begin the authentication process. Facebook will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
diff --git a/doc/integration/facebook_api_keys.png b/doc/integration/facebook_api_keys.png
new file mode 100644
index 00000000000..d6c44ac0f11
--- /dev/null
+++ b/doc/integration/facebook_api_keys.png
Binary files differ
diff --git a/doc/integration/facebook_app_settings.png b/doc/integration/facebook_app_settings.png
new file mode 100644
index 00000000000..30dd21e198a
--- /dev/null
+++ b/doc/integration/facebook_app_settings.png
Binary files differ
diff --git a/doc/integration/facebook_website_url.png b/doc/integration/facebook_website_url.png
new file mode 100644
index 00000000000..dc3088bb2fa
--- /dev/null
+++ b/doc/integration/facebook_website_url.png
Binary files differ
diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md
index 3bc5df21ef4..7e2920b8865 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -71,7 +71,7 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
# Filter LDAP users
#
- # Format: RFC 4515 http://tools.ietf.org/search/rfc4515
+ # Format: RFC 4515 https://tools.ietf.org/search/rfc4515
# Ex. (employeeType=developer)
#
# Note: GitLab does not support omniauth-ldap's custom filter syntax.
@@ -145,7 +145,7 @@ If multiple LDAP email attributes are present, e.g. `mail: foo@bar.com` and `ema
## Using an LDAP filter to limit access to your GitLab server
If you want to limit all GitLab access to a subset of the LDAP users on your LDAP server you can set up an LDAP user filter.
-The filter must comply with [RFC 4515](http://tools.ietf.org/search/rfc4515).
+The filter must comply with [RFC 4515](https://tools.ietf.org/search/rfc4515).
```ruby
# For omnibus packages; new LDAP server syntax
@@ -173,3 +173,23 @@ Tip: if you want to limit access to the nested members of an Active Directory gr
```
Please note that GitLab does not support the custom filter syntax used by omniauth-ldap.
+
+## Limitations
+
+GitLab's LDAP client is based on [omniauth-ldap](https://gitlab.com/gitlab-org/omniauth-ldap)
+which encapsulates Ruby's `Net::LDAP` class. It provides a pure-Ruby implementation
+of the LDAP client protocol. As a result, GitLab is limited by `omniauth-ldap` and may impact your LDAP
+server settings.
+
+### TLS Client Authentication
+Not implemented by `Net::LDAP`.
+So you should disable anonymous LDAP authentication and enable simple or SASL
+authentication. TLS client authentication setting in your LDAP server cannot be
+mandatory and clients cannot be authenticated with the TLS protocol.
+
+### TLS Server Authentication
+Not supported by GitLab's configuration options.
+When setting `method: ssl`, the underlying authentication method used by
+`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with
+the LDAP server before any LDAP-protocol data is exchanged but no validation of
+the LDAP server's SSL certificate is performed. \ No newline at end of file
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index c5cecbc2f2d..f2b1721fc03 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -36,7 +36,7 @@ If you want to change these settings:
```
gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_allow_single_sign_on'] = false
- gitlab_rails['block_auto_created_users'] = true
+ gitlab_rails['omniauth_block_auto_created_users'] = true
```
* **For installations from source**
@@ -73,8 +73,9 @@ Now we can choose one or more of the Supported Providers below to continue confi
- [Bitbucket](bitbucket.md)
- [GitLab.com](gitlab.md)
- [Google](google.md)
-- [Shibboleth](shibboleth.md)
+- [Facebook](facebook.md)
- [Twitter](twitter.md)
+- [Shibboleth](shibboleth.md)
- [SAML](saml.md)
- [Crowd](crowd.md)
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index 13bf15fcf45..7b94506c297 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -22,4 +22,4 @@ You accept and agree to the following terms and conditions for Your present and
8. It is your responsibility to notify GitLab B.V. when any change is required to the list of designated employees authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V..
-This text is licensed under the [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
+This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md
index 72b01433dd0..f97c252fd7c 100644
--- a/doc/legal/individual_contributor_license_agreement.md
+++ b/doc/legal/individual_contributor_license_agreement.md
@@ -22,4 +22,4 @@ You accept and agree to the following terms and conditions for Your present and
8. You agree to notify GitLab B.V. of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
-This text is licensed under the [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
+This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md
index d83838127c9..bc8e7d155e7 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -43,7 +43,7 @@ You can also use other rich text files in GitLab. You might have to install a de
## Newlines
-GFM honors the markdown specification in how [paragraphs and line breaks are handled](http://daringfireball.net/projects/markdown/syntax#p).
+GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
Line-breaks, or softreturns, are rendered if you end a line with two or more spaces
@@ -72,14 +72,14 @@ do_this_and_do_that_and_another_thing
GFM will autolink almost any URL you copy and paste into your text.
- * http://www.google.com
+ * https://www.google.com
* https://google.com/
* ftp://ftp.us.debian.org/debian/
* smb://foo/bar/baz
* irc://irc.freenode.net/gitlab
* http://localhost:3000
-* http://www.google.com
+* https://www.google.com
* https://google.com/
* ftp://ftp.us.debian.org/debian/
* smb://foo/bar/baz
@@ -274,7 +274,7 @@ The IDs are generated from the content of the header according to the following
1. All spaces are converted to hyphens
1. Two or more hyphens in a row are converted to one
1. If a header with the same ID has already been generated, a unique
- incrementing number is appended.
+ incrementing number is appended, starting at 1.
For example:
@@ -291,8 +291,8 @@ Would generate the following link IDs:
1. `this-header-has-spaces-in-it`
1. `this-header-has-a-in-it`
1. `this-header-has-unicode-in-it-한글`
+1. `this-header-has-spaces-in-it`
1. `this-header-has-spaces-in-it-1`
-1. `this-header-has-spaces-in-it-2`
Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
@@ -390,7 +390,7 @@ There are two ways to create links, inline-style and reference-style.
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
- [link text itself]: http://www.reddit.com
+ [link text itself]: https://www.reddit.com
[I'm an inline-style link](https://www.google.com)
@@ -406,7 +406,7 @@ Some text to show that the reference links can follow later.
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
-[link text itself]: http://www.reddit.com
+[link text itself]: https://www.reddit.com
**Note**
@@ -583,5 +583,5 @@ By including colons in the header row, you can align the text within that column
## References
- This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet).
-- The [Markdown Syntax Guide](http://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown.
+- The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown.
- [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown.
diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md
index 2725bf343ee..5ec0a2069b5 100644
--- a/doc/migrate_ci_to_ce/README.md
+++ b/doc/migrate_ci_to_ce/README.md
@@ -28,13 +28,20 @@ upgrade to 8.0 until you finish the migration procedure.
### Before upgrading
-If you have GitLab CI installed using omnibus-gitlab packages but *you don't want to migrate your existing data*:
+If you have GitLab CI installed using omnibus-gitlab packages but **you don't want to migrate your existing data**:
```bash
mv /var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds.$(date +%s)
```
-and run `sudo gitlab-ctl reconfigure`.
+run `sudo gitlab-ctl reconfigure` and you can reach CI at `gitlab.example.com/ci`.
+
+If you want to migrate your existing data, continue reading.
+
+#### 0. Updating Omnibus from versions prior to 7.13
+
+If you are updating from older versions you should first update to 7.14 and then to 8.0.
+Otherwise it's pretty likely that you will encounter problems described in the [Troubleshooting](#troubleshooting).
#### 1. Verify that backups work
@@ -43,6 +50,7 @@ Make sure that the backup script on both servers can connect to the database.
```
# On your CI server:
# Omnibus
+sudo chown gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds
sudo gitlab-ci-rake backup:create
# Source
@@ -143,7 +151,7 @@ sudo gitlab-ctl stop ci-sidekiq
# Source
sudo service gitlab_ci stop
cd /home/gitlab_ci/gitlab-ci
-sudo -u gitlab_ci -H bundle exec whenever --clear-crontab
+sudo -u gitlab_ci -H bundle exec whenever --clear-crontab RAILS_ENV=production
```
### II. Moving data
@@ -176,6 +184,7 @@ will need this file later.
```
# On your CI server:
# Omnibus
+sudo chown gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds
sudo gitlab-ci-rake backup:create
# Source
@@ -222,6 +231,7 @@ be no CI data yet because you turned CI on the GitLab server off earlier.
```
# On your GitLab server:
# Omnibus
+sudo chown git:git /var/opt/gitlab/gitlab-ci/builds
sudo gitlab-rake ci:migrate
# Source
@@ -319,3 +329,107 @@ You should also make sure that you can:
If something went wrong and you need to restore a backup, consult the [Backup
restoration](../raketasks/backup_restore.md) guide.
+
+### Troubleshooting
+
+#### show:secrets problem (Omnibus-only)
+If you see errors like this:
+```
+Missing `secret_key_base` or `db_key_base` for 'production' environment. The secrets will be generated and stored in `config/secrets.yml`
+rake aborted!
+Errno::EACCES: Permission denied @ rb_sysopen - config/secrets.yml
+```
+
+This can happen if you are updating from versions prior to 7.13 straight to 8.0.
+The fix for this is to update to Omnibus 7.14 first and then update it to 8.0.
+
+#### Permission denied when accessing /var/opt/gitlab/gitlab-ci/builds
+To fix that issue you have to change builds/ folder permission before doing final backup:
+```
+sudo chown -R gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds
+```
+
+Then before executing `ci:migrate` you need to fix builds folder permission:
+```
+sudo chown git:git /var/opt/gitlab/gitlab-ci/builds
+```
+
+#### Problems when importing CI database to GitLab
+If you were migrating CI database from MySQL to PostgreSQL manually you can see errros during import about missing sequences:
+```
+ALTER SEQUENCE
+ERROR: relation "ci_builds_id_seq" does not exist
+ERROR: relation "ci_commits_id_seq" does not exist
+ERROR: relation "ci_events_id_seq" does not exist
+ERROR: relation "ci_jobs_id_seq" does not exist
+ERROR: relation "ci_projects_id_seq" does not exist
+ERROR: relation "ci_runner_projects_id_seq" does not exist
+ERROR: relation "ci_runners_id_seq" does not exist
+ERROR: relation "ci_services_id_seq" does not exist
+ERROR: relation "ci_taggings_id_seq" does not exist
+ERROR: relation "ci_tags_id_seq" does not exist
+CREATE TABLE
+```
+
+To fix that you need to apply this SQL statement before doing final backup:
+```
+# Omnibus
+gitlab-ci-rails dbconsole <<EOF
+-- ALTER TABLES - DROP DEFAULTS
+ALTER TABLE ONLY ci_application_settings ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_builds ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_commits ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_events ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_jobs ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_projects ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_runner_projects ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_runners ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_services ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_taggings ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_tags ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_trigger_requests ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_triggers ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_variables ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_web_hooks ALTER COLUMN id DROP DEFAULT;
+
+-- ALTER SEQUENCES
+ALTER SEQUENCE ci_application_settings_id_seq OWNED BY ci_application_settings.id;
+ALTER SEQUENCE ci_builds_id_seq OWNED BY ci_builds.id;
+ALTER SEQUENCE ci_commits_id_seq OWNED BY ci_commits.id;
+ALTER SEQUENCE ci_events_id_seq OWNED BY ci_events.id;
+ALTER SEQUENCE ci_jobs_id_seq OWNED BY ci_jobs.id;
+ALTER SEQUENCE ci_projects_id_seq OWNED BY ci_projects.id;
+ALTER SEQUENCE ci_runner_projects_id_seq OWNED BY ci_runner_projects.id;
+ALTER SEQUENCE ci_runners_id_seq OWNED BY ci_runners.id;
+ALTER SEQUENCE ci_services_id_seq OWNED BY ci_services.id;
+ALTER SEQUENCE ci_taggings_id_seq OWNED BY ci_taggings.id;
+ALTER SEQUENCE ci_tags_id_seq OWNED BY ci_tags.id;
+ALTER SEQUENCE ci_trigger_requests_id_seq OWNED BY ci_trigger_requests.id;
+ALTER SEQUENCE ci_triggers_id_seq OWNED BY ci_triggers.id;
+ALTER SEQUENCE ci_variables_id_seq OWNED BY ci_variables.id;
+ALTER SEQUENCE ci_web_hooks_id_seq OWNED BY ci_web_hooks.id;
+
+-- ALTER TABLES - RE-APPLY DEFAULTS
+ALTER TABLE ONLY ci_application_settings ALTER COLUMN id SET DEFAULT nextval('ci_application_settings_id_seq'::regclass);
+ALTER TABLE ONLY ci_builds ALTER COLUMN id SET DEFAULT nextval('ci_builds_id_seq'::regclass);
+ALTER TABLE ONLY ci_commits ALTER COLUMN id SET DEFAULT nextval('ci_commits_id_seq'::regclass);
+ALTER TABLE ONLY ci_events ALTER COLUMN id SET DEFAULT nextval('ci_events_id_seq'::regclass);
+ALTER TABLE ONLY ci_jobs ALTER COLUMN id SET DEFAULT nextval('ci_jobs_id_seq'::regclass);
+ALTER TABLE ONLY ci_projects ALTER COLUMN id SET DEFAULT nextval('ci_projects_id_seq'::regclass);
+ALTER TABLE ONLY ci_runner_projects ALTER COLUMN id SET DEFAULT nextval('ci_runner_projects_id_seq'::regclass);
+ALTER TABLE ONLY ci_runners ALTER COLUMN id SET DEFAULT nextval('ci_runners_id_seq'::regclass);
+ALTER TABLE ONLY ci_services ALTER COLUMN id SET DEFAULT nextval('ci_services_id_seq'::regclass);
+ALTER TABLE ONLY ci_taggings ALTER COLUMN id SET DEFAULT nextval('ci_taggings_id_seq'::regclass);
+ALTER TABLE ONLY ci_tags ALTER COLUMN id SET DEFAULT nextval('ci_tags_id_seq'::regclass);
+ALTER TABLE ONLY ci_trigger_requests ALTER COLUMN id SET DEFAULT nextval('ci_trigger_requests_id_seq'::regclass);
+ALTER TABLE ONLY ci_triggers ALTER COLUMN id SET DEFAULT nextval('ci_triggers_id_seq'::regclass);
+ALTER TABLE ONLY ci_variables ALTER COLUMN id SET DEFAULT nextval('ci_variables_id_seq'::regclass);
+ALTER TABLE ONLY ci_web_hooks ALTER COLUMN id SET DEFAULT nextval('ci_web_hooks_id_seq'::regclass);
+EOF
+
+# Source
+cd /home/gitlab_ci/gitlab-ci
+sudo -u gitlab_ci -H bundle exec rails dbconsole production <<EOF
+... COPY SQL STATEMENTS FROM ABOVE ...
+EOF
+```
diff --git a/doc/operations/unicorn.md b/doc/operations/unicorn.md
index 31b432cd411..bad61151bda 100644
--- a/doc/operations/unicorn.md
+++ b/doc/operations/unicorn.md
@@ -52,7 +52,7 @@ leak memory, probably because it does not handle user requests.)
To make these memory leaks manageable, GitLab comes with the
[unicorn-worker-killer gem](https://github.com/kzk/unicorn-worker-killer). This
-gem [monkey-patches](http://en.wikipedia.org/wiki/Monkey_patch) the Unicorn
+gem [monkey-patches](https://en.wikipedia.org/wiki/Monkey_patch) the Unicorn
workers to do a memory self-check after every 16 requests. If the memory of the
Unicorn worker exceeds a pre-set limit then the worker process exits. The
Unicorn master then automatically replaces the worker process.
@@ -78,9 +78,9 @@ threshold is a random value between 200 and 250 MB. The master process (PID
```
One other thing that stands out in the log snippet above, taken from
-Gitlab.com, is that 'worker 4' was serving requests for only 23 seconds. This
+GitLab.com, is that 'worker 4' was serving requests for only 23 seconds. This
is a normal value for our current GitLab.com setup and traffic.
The high frequency of Unicorn memory restarts on some GitLab sites can be a
source of confusion for administrators. Usually they are a [red
-herring](http://en.wikipedia.org/wiki/Red_herring).
+herring](https://en.wikipedia.org/wiki/Red_herring).
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index 7a6a1958445..bcd00cfc6bf 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -6,6 +6,9 @@ If a user is both in a project group and in the project itself, the highest perm
If a user is a GitLab administrator they receive all permissions.
+On public projects the Guest role is not enforced.
+All users will be able to create issues, leave comments, and pull or download the project code.
+
To add or import a user, you can follow the [project users and members
documentation](doc/workflow/add-user/add-user.md).
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index bd439f7c6f3..6e22ea7b72a 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -1,44 +1,59 @@
# Public access
-GitLab allows you to open selected projects to be accessed **publicly** or **internally**.
+GitLab allows you to change your projects' visibility in order be accessed
+**publicly** or **internally**.
-Projects with either of these visibility levels will be listed in the [public access directory](/public).
+Projects with either of these visibility levels will be listed in the
+public access directory (`/public` under your GitLab instance).
+Here is the [GitLab.com example](https://gitlab.com/public).
Internal projects will only be available to authenticated users.
-## Public projects
+## Visibility of projects
+
+### Public projects
Public projects can be cloned **without any** authentication.
-It will also be listed on the [public access directory](/public).
+They will also be listed on the public access directory (`/public`).
-**Any logged in user** will have [Guest](../permissions/permissions) permissions on the repository.
+**Any logged in user** will have [Guest](../permissions/permissions)
+permissions on the repository.
-## Internal projects
+### Internal projects
Internal projects can be cloned by any logged in user.
-It will also be listed on the [public access directory](/public) for logged in users.
+They will also be listed on the public access directory (`/public`) for logged
+in users.
-Any logged in user will have [Guest](../permissions/permissions) permissions on the repository.
+Any logged in user will have [Guest](../permissions/permissions) permissions on
+the repository.
-## How to change project visibility
+### How to change project visibility
-1. Go to your project dashboard
-1. Click on the "Edit" tab
-1. Change "Visibility Level"
+1. Go to your project's **Settings**
+1. Change "Visibility Level" to either Public, Internal or Private
## Visibility of users
-The public page of users, located at `/u/username` is visible if either:
+The public page of a user, located at `/u/username`, is always visible whether
+you are logged in or not.
+
+When visiting the public page of a user, you can only see the projects which
+you are privileged to.
-- You are logged in.
-- You are logged out, and the target user is authorized to (is Guest, Reporter, etc.) at least one public project.
+## Visibility of groups
-Otherwise, you will be redirected to the sign in page.
+The public page of a group, located at `/groups/groupname`, is always visible
+to everyone.
-When visiting the public page of an user, you will only see listed projects which you can view yourself.
+Logged out users will be able to see the description and the avatar of the
+group as well as all public projects belonging to that group.
## Restricting the use of public or internal projects
-In the Admin area under Settings you can disable public projects or public and internal projects for the entire GitLab installation to prevent people making code public by accident. The restricted visibility settings do not apply to admin users.
+In the Admin area under **Settings** (`/admin/application_settings`), you can
+restrict the use of visibility levels for users when they create a project or a
+snippet. This is useful to prevent people exposing their repositories to public
+by accident. The restricted visibility settings do not apply to admin users.
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 4ff5e74d438..b4d2786bd76 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -7,14 +7,16 @@
A backup creates an archive file that contains the database, all repositories and all attachments.
This archive will be saved in backup_path (see `config/gitlab.yml`).
The filename will be `[TIMESTAMP]_gitlab_backup.tar`. This timestamp can be used to restore an specific backup.
-You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. The best way to migrate your repositories from one server to another is through backup restore.
+You can only restore a backup to exactly the same version of GitLab that you created it
+on, for example 7.2.1. The best way to migrate your repositories from one server to
+another is through backup restore.
You need to keep a separate copy of `/etc/gitlab/gitlab-secrets.json`
(for omnibus packages) or `/home/git/gitlab/.secret` (for installations
from source). This file contains the database encryption key used
for two-factor authentication. If you restore a GitLab backup without
restoring the database encryption key, users who have two-factor
-authentication enabled will loose access to your GitLab server.
+authentication enabled will lose access to your GitLab server.
If you are interested in GitLab CI backup please follow to the [CI backup documentation](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/raketasks/backup_restore.md)*
@@ -27,7 +29,8 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
Also you can choose what should be backed up by adding environment variable SKIP. Available options: db,
-uploads (attachments), repositories. Use a comma to specify several options at the same time.
+uploads (attachments), repositories, builds(CI build output logs), artifacts (CI build artifacts), lfs (LFS objects).
+Use a comma to specify several options at the same time.
```
sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
@@ -95,6 +98,8 @@ For installations from source:
aws_secret_access_key: 'secret123'
# The remote 'directory' to store your backups. For S3, this would be the bucket name.
remote_directory: 'my.s3.bucket'
+ # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
+ # encryption: 'AES256'
```
If you are uploading your backups to S3 you will probably want to create a new
@@ -269,9 +274,6 @@ sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186
# Start GitLab
sudo gitlab-ctl start
-# Create satellites
-sudo gitlab-rake gitlab:satellites:create
-
# Check GitLab
sudo gitlab-rake gitlab:check SANITIZE=true
```
@@ -369,7 +371,11 @@ For more information see similar questions on postgresql issue tracker[here](htt
## Note
This documentation is for GitLab CE.
-We backup GitLab.com and make sure your data is secure, but you can't use these methods
-to export / backup your data yourself from GitLab.com.
+We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com.
Issues are stored in the database. They can't be stored in Git itself.
+
+To migrate your repositories from one server to another with an up-to-date version of
+GitLab, you can use the [import rake task](import.md) to do a mass import of the
+repository. Note that if you do an import rake task, rather than a backup restore, you
+will have all your repositories, but not any other data.
diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md
index 96d67f7b5d6..8fbcbb983e9 100644
--- a/doc/raketasks/cleanup.md
+++ b/doc/raketasks/cleanup.md
@@ -12,7 +12,8 @@ sudo gitlab-rake gitlab:cleanup:dirs
bundle exec rake gitlab:cleanup:dirs RAILS_ENV=production
```
-Remove repositories (global only for now) from `/home/git/repositories` if they don't exist in GitLab database.
+Rename repositories from `/home/git/repositories` if they don't exist in GitLab database.
+The repositories get a `+orphaned+TIMESTAMP` suffix so that they cannot block new repositories from being created.
```
# omnibus-gitlab
diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index 4fbd20762da..629d38efc53 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -56,3 +56,17 @@ bundle exec rake gitlab:import:all_users_to_all_groups RAILS_ENV=production
```
block_auto_created_users: false
```
+
+## Disable Two-factor Authentication (2FA) for all users
+
+This task will disable 2FA for all users that have it enabled. This can be
+useful if GitLab's `.secret` file has been lost and users are unable to login,
+for example.
+
+```bash
+# omnibus-gitlab
+sudo gitlab-rake gitlab:two_factor:disable_for_all_users
+
+# installation from source
+bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
+```
diff --git a/doc/release/README.md b/doc/release/README.md
index 1342b90f3b3..52eca7c02a6 100644
--- a/doc/release/README.md
+++ b/doc/release/README.md
@@ -1,4 +1,8 @@
-GitLab has the following updates:
+## Release cycle
+
+Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). Features that will likely be in the next releases can be found on the [direction page](https://about.gitlab.com/direction/).
+
+## Release process documentation
- [Monthly release](monthly.md), every month on the 22nd.
- [Patch release](patch.md), if there are serious regressions.
diff --git a/doc/release/monthly.md b/doc/release/monthly.md
index c56e99a7005..aff3f066b24 100644
--- a/doc/release/monthly.md
+++ b/doc/release/monthly.md
@@ -25,69 +25,88 @@ If the release is falling behind immediately warn the team.
## Create an overall issue and follow it
-Create issue for GitLab CE project(internal). Name it "Release x.x.x" for easier searching.
-Replace the dates with actual dates based on the number of workdays before the release.
-All steps from issue template are explained below
+Create an issue in the GitLab CE project. Name it "Release x.x" and tag it with
+the `release` label for easier searching. Replace the dates with actual dates
+based on the number of workdays before the release. All steps from issue
+template are explained below:
```
-Xth: (7 working days before the 22nd)
+### Xth: (7 working days before the 22nd)
-- [ ] Triage the omnibus-gitlab milestone
+- [ ] Triage the [Omnibus milestone]
-Xth: (6 working days before the 22nd)
+### Xth: (6 working days before the 22nd)
-- [ ] Merge CE master in to EE master via merge request (#LINK)
- [ ] Determine QA person and notify this person
- [ ] Check the tasks in [how to rc1 guide](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/release/howto_rc1.md) and delegate tasks if necessary
-- [ ] Create CE, EE, CI RC1 versions (#LINK)
-- [ ] Build RC1 packages (EE first) (#LINK)
+- [ ] Merge CE `master` into EE `master` via merge request (#LINK)
+- [ ] Create CE and EE RC1 versions (#LINK)
+- [ ] Build RC1 packages
-Xth: (5 working days before the 22nd)
+### Xth: (5 working days before the 22nd)
- [ ] Do QA and fix anything coming out of it (#LINK)
-- [ ] Close the omnibus-gitlab milestone
-- [ ] Prepare the blog post (#LINK)
+- [ ] Close the [Omnibus milestone]
+- [ ] Prepare the [blog post]
-Xth: (4 working days before the 22nd)
+### Xth: (4 working days before the 22nd)
-- [ ] Update GitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package)
-- [ ] Update ci.gitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package)
-- [ ] Create regression issues (CE, CI) (#LINK)
-- [ ] Tweet about rc1 (#LINK), proposed text:
+- [ ] Update GitLab.com with RC1
+- [ ] Create the regression issue in the CE issue tracker:
-> GitLab x.x.0.rc1 is available https://packages.gitlab.com/gitlab/unstable Use at your own risk. Please link regressions issues from LINK_TO_REGRESSION_ISSUE
+ ```
+ This is a meta issue to index possible regressions in this monthly release
+ and any patch versions.
-Xth: (3 working days before the 22nd)
+ Please do not raise or discuss issues directly in this issue but link to
+ issues that might warrant a patch release. If there is a Merge Request
+ that fixes the issue, please link to that as well.
-- [ ] Merge CE stable branch into EE stable branch
+ Please only post one regression issue and/or merge request per comment.
+ Comments will be updated by the release manager as they are addressed.
+ ```
-Xth: (2 working days before the 22nd)
+- [ ] Tweet about RC1 release:
-- [ ] Check that everyone is mentioned on the blog post using `@all` (the reviewer should have done this one working day ago)
-- [ ] Check that MVP is added to the mvp page (source/mvp/index.html in www-gitlab-com)
+ ```
+ GitLab x.y.0.rc1 is available: https://packages.gitlab.com/gitlab/unstable
+ Use at your own risk. Please link regressions issues from
+ LINK_TO_REGRESSION_ISSUE
+ ```
-Xth: (1 working day before the 22nd)
+### Xth: (3 working days before the 22nd)
-- [ ] Merge CE stable into EE stable
-- [ ] Create CE, EE, CI release candidates (#LINK) (hopefully final ones with the same commit as the release tomorrow)
+- [ ] Merge `x-y-stable` into `x-y-stable-ee`
+- [ ] Check that everyone is mentioned on the [blog post] using `@all`
+
+### Xth: (2 working days before the 22nd)
+
+- [ ] Check that MVP is added to the [MVP page]
+
+### Xth: (1 working day before the 22nd)
+
+- [ ] Merge `x-y-stable` into `x-y-stable-ee`
+- [ ] Create CE and EE release candidates
- [ ] Create Omnibus tags and build packages for the latest release candidates
-- [ ] Update GitLab.com with the latest RC (#LINK)
-- [ ] Update ci.gitLab.com with the latest RC (#LINK)
+- [ ] Update GitLab.com with the latest RC
-22nd before 12AM CET:
+### 22nd before 1200 CET:
-Release before 12AM CET / 3AM PST, to make sure the majority of our users
+Release before 1200 CET / 2AM PST, to make sure the majority of our users
get the new version on the 22nd and there is sufficient time in the European
workday to quickly fix any issues.
-- [ ] Merge CE stable into EE stable (#LINK)
-- [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools) (#LINK)
+- [ ] Merge `x-y-stable` into `x-y-stable-ee`
+- [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools)
- [ ] Create the 'x.y.0' version on version.gitlab.com
-- [ ] Try to do before 11AM CET: Create and push omnibus tags for x.y.0 (will auto-release the packages) (#LINK)
-- [ ] Try to do before 12AM CET: Publish the release blog post (#LINK)
-- [ ] Tweet about the release (blog post) (#LINK)
-- [ ] Schedule a second tweet of the release announcement with the same text at 6PM CET / 9AM PST
-
+- [ ] Try to do before 1100 CET: Create and push Omnibus tags for x.y.0 (will auto-release the packages)
+- [ ] Try to do before 1200 CET: Publish the release [blog post]
+- [ ] Tweet about the release
+- [ ] Schedule a second Tweet of the release announcement with the same text at 1800 CET / 8AM PST
+
+[Omnibus milestone]: LINK_TO_OMNIBUS_MILESTONE
+[blog post]: LINK_TO_WIP_BLOG_POST
+[MVP page]: https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/source/mvp/index.html
```
- - -
@@ -140,7 +159,7 @@ Please do not raise issues directly in this issue but link to issues that might
The decision to create a patch release or not is with the release manager who is assigned to this issue.
The release manager will comment here about the plans for patch releases.
-Assign the issue to the release manager and at mention all members of gitlab core team. If there are any known bugs in the release add them immediately.
+Assign the issue to the release manager and at mention all members of GitLab core team. If there are any known bugs in the release add them immediately.
## Tweet about RC1
@@ -223,4 +242,4 @@ Consider creating a post on Hacker News.
## Create a WIP blogpost for the next release
-Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). \ No newline at end of file
+Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md).
diff --git a/doc/release/patch.md b/doc/release/patch.md
index 6aa11b283df..3022e375aca 100644
--- a/doc/release/patch.md
+++ b/doc/release/patch.md
@@ -1,21 +1,46 @@
# Things to do when doing a patch release
-NOTE: This is a guide for GitLab developers. If you are trying to install GitLab see the latest stable [installation guide](install/installation.md) and if you are trying to upgrade, see the [upgrade guides](update).
+NOTE: This is a guide for GitLab developers. If you are trying to install GitLab
+see the latest stable [installation guide](install/installation.md) and if you
+are trying to upgrade, see the [upgrade guides](update).
## When to do a patch release
-Do a patch release when there is a critical regression that needs to be addresses before the next monthly release.
-
-Otherwise include it in the monthly release and note there was a regression fix in the release announcement.
+Patch releases are done as-needed in order to fix regressions in the current
+major release that cannot or should not wait until the next major release.
+What's included and when to release is at the discretion of the release manager.
## Release Procedure
+### Create a patch issue
+
+Create an issue in the GitLab CE project. Name it "Release x.y.z", tag it with
+the `release` label, and assign it to the milestone of the corresponding major
+release.
+
+Use the following template:
+
+```
+- Picked into respective `stable` branches:
+- [ ] Merge `x-y-stable` into `x-y-stable-ee`
+- [ ] release-tools: `x.y.z`
+- gitlab-omnibus
+ - [ ] `x.y.z+ee.0`
+ - [ ] `x.y.z+ce.0`
+- [ ] Deploy
+- [ ] Add patch notice to [x.y regressions]()
+- [ ] [Blog post]()
+- [ ] [Tweet]()
+- [ ] Add entry to version.gitlab.com
+```
+
+Update the issue with links to merge requests that need to be/have been picked
+into the `stable` branches.
+
### Preparation
1. Verify that the issue can be reproduced
1. Note in the 'GitLab X.X regressions' that you will create a patch
-1. Create an issue on private GitLab development server
-1. Name the issue "Release X.X.X CE and X.X.X EE", this will make searching easier
1. Fix the issue on a feature branch, do this on the private GitLab development server
1. If it is a security issue, then assign it to the release manager and apply a 'security' label
1. Consider creating and testing workarounds
@@ -25,7 +50,6 @@ Otherwise include it in the monthly release and note there was a regression fix
1. For EE, update the CHANGELOG-EE if it is EE specific fix. Otherwise, merge the stable CE branch and add to CHANGELOG-EE "Merge community edition changes for version X.X.X"
1. Merge CE stable branch into EE stable branch
-
### Bump version
Get release tools
@@ -54,4 +78,4 @@ bundle exec rake release["x.x.x"]
1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only)
1. Create the 'x.y.0' version on version.gitlab.com
1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
-1. Create a new patch release issue for the next potential release \ No newline at end of file
+1. Create a new patch release issue for the next potential release
diff --git a/doc/release/security.md b/doc/release/security.md
index 60bcfbb6da5..b1a62b333e6 100644
--- a/doc/release/security.md
+++ b/doc/release/security.md
@@ -8,7 +8,7 @@ Do a security release when there is a critical issue that needs to be addresses
## Security vulnerability disclosure
-Please report suspected security vulnerabilities in private to <support@gitlab.com>, also see the [disclosure section on the GitLab.com website](http://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
+Please report suspected security vulnerabilities in private to <support@gitlab.com>, also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
## Release Procedure
@@ -25,7 +25,7 @@ Please report suspected security vulnerabilities in private to <support@gitlab.c
1. Send tweets about the release from `@gitlabhq`
1. Send out an email to [the community google mailing list](https://groups.google.com/forum/#!forum/gitlabhq)
1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number. CVE is only needed for bugs that allow someone to own the server (Remote Code Execution) or access to code of projects they are not a member of.
-1. Add the security researcher to the [Security Researcher Acknowledgments list](http://about.gitlab.com/vulnerability-acknowledgements/)
+1. Add the security researcher to the [Security Researcher Acknowledgments list](https://about.gitlab.com/vulnerability-acknowledgements/)
1. Thank the security researcher in an email for their cooperation
1. Update the blog post and the CHANGELOG when we receive the CVE number
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index b6b8000af4e..fe5b45dd432 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -78,11 +78,11 @@ Deploy keys can be shared between projects, you just need to add them to each pr
### Eclipse
-How to add your ssh key to Eclipse: http://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
+How to add your ssh key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
## Tip: Non-default OpenSSH key file names or locations
-If, for whatever reason, you decide to specify a non-default location and filename for your Gitlab SSH key pair, you must configure your SSH client to find your Gitlab SSH private key for connections to your Gitlab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following:
+If, for whatever reason, you decide to specify a non-default location and filename for your GitLab SSH key pair, you must configure your SSH client to find your GitLab SSH private key for connections to your GitLab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following:
```
#
@@ -97,7 +97,7 @@ User mygitlabusername
Another example
```
#
-# Our company's internal Gitlab server
+# Our company's internal GitLab server
#
Host my-gitlab.company.com
RSAAuthentication yes
@@ -109,4 +109,4 @@ Note in the gitlab.com example above a username was specified to override the de
Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document.
Public SSH keys need to be unique, as they will bind to your account. Your SSH key is the only identifier you'll
-have when pushing code via SSH. That's why it needs to uniquely map to a single user.
+have when pushing code via SSH. That's why it needs to uniquely map to a single user. \ No newline at end of file
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index b0e4613cdef..5cb05b13b3e 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -48,16 +48,17 @@ X-Gitlab-Event: System Hook
```json
{
- "created_at": "2012-07-21T07:30:56Z",
- "event_name": "user_add_to_team",
- "project_access": "Master",
- "project_id": 74,
- "project_name": "StoreCloud",
- "project_path": "storecloud",
- "user_email": "johnsmith@gmail.com",
- "user_name": "John Smith",
- "user_id": 41,
- "project_visibility": "private",
+ "created_at": "2012-07-21T07:30:56Z",
+ "event_name": "user_add_to_team",
+ "project_access": "Master",
+ "project_id": 74,
+ "project_name": "StoreCloud",
+ "project_path": "storecloud",
+ "project_path_with_namespace": "jsmith/storecloud",
+ "user_email": "johnsmith@gmail.com",
+ "user_name": "John Smith",
+ "user_id": 41,
+ "project_visibility": "private",
}
```
@@ -65,16 +66,17 @@ X-Gitlab-Event: System Hook
```json
{
- "created_at": "2012-07-21T07:30:56Z",
- "event_name": "user_remove_from_team",
- "project_access": "Master",
- "project_id": 74,
- "project_name": "StoreCloud",
- "project_path": "storecloud",
- "user_email": "johnsmith@gmail.com",
- "user_name": "John Smith",
- "user_id": 41,
- "project_visibility": "private",
+ "created_at": "2012-07-21T07:30:56Z",
+ "event_name": "user_remove_from_team",
+ "project_access": "Master",
+ "project_id": 74,
+ "project_name": "StoreCloud",
+ "project_path": "storecloud",
+ "project_path_with_namespace": "jsmith/storecloud",
+ "user_email": "johnsmith@gmail.com",
+ "user_name": "John Smith",
+ "user_id": 41,
+ "project_visibility": "private",
}
```
diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md
index b34fb12da6f..4516a102084 100644
--- a/doc/update/6.x-or-7.x-to-7.14.md
+++ b/doc/update/6.x-or-7.x-to-7.14.md
@@ -47,7 +47,7 @@ Download and compile Ruby:
```bash
mkdir /tmp/ruby && cd /tmp/ruby
-curl --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz
+curl --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz
cd ruby-2.1.6
./configure --disable-install-rdoc
make
diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md
index 0cb82fb718a..305017b7048 100644
--- a/doc/update/7.14-to-8.0.md
+++ b/doc/update/7.14-to-8.0.md
@@ -84,6 +84,7 @@ Now we download `gitlab-git-http-server` and install it in `/home/git/gitlab-git
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git
cd gitlab-git-http-server
+sudo -u git -H git checkout 0.2.14
sudo -u git -H make
```
@@ -168,18 +169,28 @@ git diff origin/7-14-stable:lib/support/nginx/gitlab origin/8-0-stable:lib/suppo
```
If you are using Apache instead of NGINX please see the updated [Apache templates](https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache).
+Also note that because Apache does not support upstreams behind Unix sockets you will need to let gitlab-git-http-server listen on a TCP port. You can do this via [/etc/default/gitlab](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-0-stable/lib/support/init.d/gitlab.default.example#L34).
### 9. Migrate GitLab CI to GitLab CE/EE
Now, GitLab CE and EE has CI integrated. However, migrations don't happen automatically and you need to do it manually.
Please follow the following guide [to migrate](../migrate_ci_to_ce/README.md) your GitLab CI instance to GitLab CE/EE.
-### 10. Start application
+### 10. Use Redis v2.4.0+
+
+Previous versions of GitLab allowed Redis versions >= 2.0 to be used, but
+Sidekiq jobs could fail due to lack of support for the SREM command. GitLab
+8.0 now checks that Redis >= 2.4.0 is used. You can check your Redis version
+with the following command:
+
+ redis-cli info | grep redis_version
+
+### 11. Start application
sudo service gitlab start
sudo service nginx restart
-### 11. Check application status
+### 12. Check application status
Check if GitLab and its environment are configured correctly:
diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md
new file mode 100644
index 00000000000..d57c0d0674d
--- /dev/null
+++ b/doc/update/8.0-to-8.1.md
@@ -0,0 +1,171 @@
+# From 8.0 to 8.1
+
+**NOTE:** GitLab 8.0 introduced several significant changes related to
+installation and configuration which *are not duplicated here*. Be sure you're
+already running a working version of 8.0 before proceeding with this guide.
+
+### 0. Double-check your Git version
+
+**This notice applies only to /usr/local/bin/git**
+
+If you compiled Git from source on your GitLab server then please double-check
+that you are using a version that protects against CVE-2014-9390. For six
+months after this vulnerability became known the GitLab installation guide
+still contained instructions that would install the outdated, 'vulnerable' Git
+version 2.1.2.
+
+Run the following command to get your current Git version:
+
+```sh
+/usr/local/bin/git --version
+```
+
+If you see 'No such file or directory' then you did not install Git according
+to the outdated instructions from the GitLab installation guide and you can go
+to the next step 'Stop server' below.
+
+If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4,
+v2.2.1 or newer. You can use the [instructions in the GitLab source
+installation
+guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies)
+to install a newer version of Git.
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 8-1-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-1-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch
+sudo -u git -H git checkout v2.6.5
+```
+
+### 5. Update gitlab-git-http-server
+
+```bash
+cd /home/git/gitlab-git-http-server
+sudo -u git -H git fetch origin
+sudo -u git -H git checkout 0.3.0
+sudo -u git -H make
+```
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+
+# Update init.d script
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+### 7. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-0-stable:config/gitlab.yml.example origin/8-1-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+View changes between the previous recommended Nginx configuration and the
+current one:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-0-stable:lib/support/nginx/gitlab-ssl origin/8-1-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-0-stable:lib/support/nginx/gitlab origin/8-1-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-git-http-server listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-1-stable/lib/support/init.d/gitlab.default.example#L34
+
+### 8. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 9. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.0)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 7.14 to 8.0](7.14-to-8.0.md), except for the database migration
+(The backup is already migrated to the previous version)
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+## Troubleshooting
+
+### "You appear to have cloned an empty repository."
+
+See the [7.14 to 8.0 update guide](7.14-to-8.0.md#troubleshooting).
diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md
new file mode 100644
index 00000000000..7b228d6a22f
--- /dev/null
+++ b/doc/update/8.1-to-8.2.md
@@ -0,0 +1,189 @@
+# From 8.1 to 8.2
+
+**NOTE:** GitLab 8.0 introduced several significant changes related to
+installation and configuration which *are not duplicated here*. Be sure you're
+already running a working version of at least 8.0 before proceeding with this
+guide.
+
+### 0. Double-check your Git version
+
+**This notice applies only to /usr/local/bin/git**
+
+If you compiled Git from source on your GitLab server then please double-check
+that you are using a version that protects against CVE-2014-9390. For six
+months after this vulnerability became known the GitLab installation guide
+still contained instructions that would install the outdated, 'vulnerable' Git
+version 2.1.2.
+
+Run the following command to get your current Git version:
+
+```sh
+/usr/local/bin/git --version
+```
+
+If you see 'No such file or directory' then you did not install Git according
+to the outdated instructions from the GitLab installation guide and you can go
+to the next step 'Stop server' below.
+
+If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4,
+v2.2.1 or newer. You can use the [instructions in the GitLab source
+installation
+guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies)
+to install a newer version of Git.
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 8-2-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-2-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch
+sudo -u git -H git checkout v2.6.8
+```
+
+### 5. Replace gitlab-git-http-server with gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires [Go
+1.5](https://golang.org/dl) which should already be on your system
+from GitLab 8.1.
+
+```bash
+cd /home/git
+sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
+cd gitlab-workhorse
+sudo -u git -H git checkout 0.4.2
+sudo -u git -H make
+```
+
+Update the GitLab init script and 'default' file.
+
+```
+cd /home/git/gitlab
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+test -e /etc/default/gitlab && \
+ sudo sed -i.pre-8.2 's/^\([^=]*\)gitlab_git_http_server/\1gitlab_workhorse/' /etc/default/gitlab
+```
+
+Make sure that you also update your **NGINX configuration** to use
+the new gitlab-workhorse.socket file.
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+
+# Update init.d script
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+### 7. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-1-stable:config/gitlab.yml.example origin/8-2-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+View changes between the previous recommended Nginx configuration and the
+current one:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-1-stable:lib/support/nginx/gitlab-ssl origin/8-2-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-1-stable:lib/support/nginx/gitlab origin/8-2-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-git-http-server listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-2-stable/lib/support/init.d/gitlab.default.example#L34
+
+### 8. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 9. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.1)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.0 to 8.1](8.0-to-8.1.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+## Troubleshooting
+
+### "You appear to have cloned an empty repository."
+
+See the [7.14 to 8.0 update guide](7.14-to-8.0.md#troubleshooting).
diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index a596ea38456..a7de5648c0e 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -60,6 +60,9 @@ sudo -u git -H python mysql-postgresql-converter/db_converter.py gitlabhq_produc
sudo -u git -H ed -s db/database.sql < mysql-postgresql-converter/move_drop_indexes.ed
# Compress database backup
+# Warning: If you have Gitlab 7.12.0 or older skip this step and import the database.sql directly into the backup with:
+# sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql
+# The compressed databasedump is not supported at 7.12.0 and older.
sudo -u git -H gzip db/database.sql
# Replace the MySQL dump in TIMESTAMP_gitlab_backup.tar.
@@ -71,4 +74,5 @@ sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql.gz
# Done! TIMESTAMP_gitlab_backup.tar can now be restored into a Postgres GitLab
# installation.
+# See https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/raketasks/backup_restore.md for more information about backups.
```
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index a66a863f6c4..957354decb7 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -6,7 +6,8 @@ For example from 7.14.0 to 7.14.3, also see the [semantic versioning specificati
### 0. Backup
It's useful to make a backup just in case things go south:
-(With MySQL, this may require granting "LOCK TABLES" privileges to the GitLab user on the database version)
+(With MySQL, this may require granting "LOCK TABLES" privileges to the GitLab
+user on the database version)
```bash
cd /home/git/gitlab
@@ -15,16 +16,22 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
### 1. Stop server
- sudo service gitlab stop
+```bash
+sudo service gitlab stop
+```
### 2. Get latest code for the stable branch
+In the commands below, replace `LATEST_TAG` with the latest GitLab tag you want
+to update to, for example `v8.0.3`. Use `git tag -l 'v*.[0-9]' --sort='v:refname'`
+to see a list of all tags. Make sure to update patch versions only (check your
+current version with `cat VERSION`).
+
```bash
cd /home/git/gitlab
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- Gemfile.lock db/schema.rb
-LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)
-sudo -u git -H git checkout $LATEST_TAG -b $LATEST_TAG
+sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG
```
### 3. Update gitlab-shell to the corresponding version
@@ -35,36 +42,48 @@ sudo -u git -H git fetch
sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`
```
-### 4. Install libs, migrations, etc.
+### 4. Update gitlab-workhorse to the corresponding version
+
+```bash
+cd /home/git/gitlab-workhorse
+sudo -u git -H git fetch
+sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION`
+```
+
+### 5. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
-#PostgreSQL
+# PostgreSQL
sudo -u git -H bundle install --without development test mysql --deployment
# MySQL
sudo -u git -H bundle install --without development test postgres --deployment
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
-sudo -u git -H bundle exec rake assets:clean RAILS_ENV=production
-sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
-sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
-### 5. Start application
+### 6. Start application
- sudo service gitlab start
- sudo service nginx restart
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
-### 6. Check application status
+### 7. Check application status
Check if GitLab and its environment are configured correctly:
- sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```bash
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
To make sure you didn't miss anything run a more thorough check with:
- sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```bash
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
If all items are green, then congratulations upgrade complete!
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index c185ccfcac3..03746dd9df3 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -57,6 +57,9 @@ X-Gitlab-Event: Push Hook
"name": "Jordi Mallach",
"email": "jordi@softcatala.org"
}
+ "added": ["CHANGELOG"],
+ "modified": ["app/controller/application.rb"],
+ "removed": []
},
{
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
@@ -66,10 +69,14 @@ X-Gitlab-Event: Push Hook
"author": {
"name": "GitLab dev user",
"email": "gitlabdev@dv6700.(none)"
- }
+ },
+ "added": ["CHANGELOG"],
+ "modified": ["app/controller/application.rb"],
+ "removed": []
}
],
"total_commits_count": 4
+
}
```
@@ -314,7 +321,8 @@ X-Gitlab-Event: Note Hook
"name": "John Smith",
"email": "john@example.com"
}
- }
+ },
+ "work_in_progress": false
}
}
```
@@ -500,6 +508,7 @@ X-Gitlab-Event: Merge Request Hook
"email": "gitlabdev@dv6700.(none)"
}
},
+ "work_in_progress": false,
"url": "http://example.com/diaspora/merge_requests/1",
"action": "open"
}
@@ -537,4 +546,4 @@ When you press 'Test Hook' in GitLab, you should see something like this in the
{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>}
example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0
- -> /
-``` \ No newline at end of file
+```
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 5b8d72dfd34..a6b4d951188 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -13,5 +13,8 @@
- [Project users](add-user/add-user.md)
- [Protected branches](protected_branches.md)
- [Web Editor](web_editor.md)
+- [Releases](releases.md)
+- [Milestones](milestones.md)
- [Merge Requests](merge_requests.md)
- ["Work In Progress" Merge Requests](wip_merge_requests.md)
+- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
diff --git a/doc/workflow/award_emoji.png b/doc/workflow/award_emoji.png
new file mode 100644
index 00000000000..fb26ee04393
--- /dev/null
+++ b/doc/workflow/award_emoji.png
Binary files differ
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index f608674faf6..8965e5b3654 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -7,7 +7,7 @@ This allows a wide variety of branching strategies and workflows.
Almost all of these are an improvement over the methods used before git.
But many organizations end up with a workflow that is not clearly defined, overly complex or not integrated with issue tracking systems.
Therefore we propose the GitLab flow as clearly defined set of best practices.
-It combines [feature driven development](http://en.wikipedia.org/wiki/Feature-driven_development) and [feature branches](http://martinfowler.com/bliki/FeatureBranch.html) with issue tracking.
+It combines [feature driven development](https://en.wikipedia.org/wiki/Feature-driven_development) and [feature branches](http://martinfowler.com/bliki/FeatureBranch.html) with issue tracking.
Organizations coming to git from other version control systems frequently find it hard to develop an effective workflow.
This article describes the GitLab flow that integrates the git workflow with an issue tracking system.
@@ -26,7 +26,7 @@ After getting used to these three steps the branching model becomes the challeng
Since many organizations new to git have no conventions how to work with it, it can quickly become a mess.
The biggest problem they run into is that many long running branches that each contain part of the changes are around.
People have a hard time figuring out which branch they should develop on or deploy to production.
-Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html)
+Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html).
We think there is still room for improvement and will detail a set of practices we call GitLab flow.
## Git flow and its problems
@@ -91,7 +91,7 @@ This workflow where commits only flow downstream ensures that everything has bee
If you need to cherry-pick a commit with a hotfix it is common to develop it on a feature branch and merge it into master with a merge request, do not delete the feature branch.
If master is good to go (it should be if you a practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches.
If this is not possible because more manual testing is required you can send merge requests from the feature branch to the downstream branches.
-An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](http://teatro.io/).
+An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](https://teatro.io/).
## Release branches with GitLab flow
@@ -104,7 +104,7 @@ By branching as late as possible you minimize the time you have to apply bug fix
After a release branch is announced, only serious bug fixes are included in the release branch.
If possible these bug fixes are first merged into master and then cherry-picked into the release branch.
This way you can't forget to cherry-pick them into master and encounter the same bug on subsequent releases.
-This is called an 'upstream first' policy that is also practiced by [Google](http://www.chromium.org/chromium-os/chromiumos-design-docs/upstream-first) and [Red Hat](http://www.redhat.com/about/news/archive/2013/5/a-community-for-using-openstack-with-red-hat-rdo).
+This is called an 'upstream first' policy that is also practiced by [Google](https://www.chromium.org/chromium-os/chromiumos-design-docs/upstream-first) and [Red Hat](https://www.redhat.com/about/news/archive/2013/5/a-community-for-using-openstack-with-red-hat-rdo).
Every time a bug-fix is included in a release branch the patch version is raised (to comply with [Semantic Versioning](http://semver.org/)) by setting a new tag.
Some projects also have a stable branch that points to the same commit as the latest released branch.
In this flow it is not common to have a production branch (or git flow master branch).
@@ -200,7 +200,7 @@ And to understand a change in context one can always look at the merge commit th
After you merge multiple commits from a feature branch into the master branch this is harder to undo.
If you would have squashed all the commits into one you could have just reverted this commit but as we indicated you should not rebase commits after they are pushed.
-Fortunately [reverting a merge made some time ago](http://git-scm.com/blog/2010/03/02/undoing-merges.html) can be done with git.
+Fortunately [reverting a merge made some time ago](https://git-scm.com/blog/2010/03/02/undoing-merges.html) can be done with git.
This however, requires having specific merge commits for the commits your want to revert.
If you revert a merge and you change your mind, revert the revert instead of merging again since git will not allow you to merge the code again otherwise.
@@ -215,7 +215,7 @@ With git you can also rebase your feature branch commits to order them after the
This prevents creating a merge commit when merging master into your feature branch and creates a nice linear history.
However, just like with squashing you should never rebase commits you have pushed to a remote server.
This makes it impossible to rebase work in progress that you already shared with your team which is something we recommend.
-When using rebase to keep your feature branch updated you [need to resolve similar conflicts again and again](http://blogs.atlassian.com/2013/10/git-team-workflows-merge-or-rebase/).
+When using rebase to keep your feature branch updated you [need to resolve similar conflicts again and again](https://blogs.atlassian.com/2013/10/git-team-workflows-merge-or-rebase/).
You can reuse recorded resolutions (rerere) sometimes, but without rebasing you only have to solve the conflicts one time and you’re set.
There has to be a better way to avoid many merge commits.
@@ -244,13 +244,12 @@ Developing software happen in small messy steps and it is OK to have your histor
You can use tools to view the network graphs of commits and understand the messy history that created your code.
If you rebase code the history is incorrect, and there is no way for tools to remedy this because they can't deal with changing commit identifiers.
-## Voting on merge requests
+## Award emojis on issues and merge requests
-![Voting slider in GitLab](voting_slider.png)
+![Emoji bar in GitLab](award_emoji.png)
-It is common to voice approval or disapproval by using +1 or -1 emoticons.
-In GitLab the +1 and -1 are aggregated and shown at the top of the merge request.
-As a rule of thumb anything that doesn't have two times more +1's than -1's is suspect and should not be merged yet.
+It is common to voice approval or disapproval by using +1 or -1. In GitLab you
+can use emojis to give a virtual high five on issues and merge requests.
## Pushing and removing branches
@@ -307,7 +306,7 @@ When initiating a feature branch, always start with an up to date master to bran
If you know beforehand that your work absolutely depends on another branch you can also branch from there.
If you need to merge in another branch after starting explain the reason in the merge commit.
If you have not pushed your commits to a shared location yet you can also rebase on master or another feature branch.
-Do not merge in upstream if your code will work and merge cleanly without doing so, Linus even says that [you should never merge in upstream at random points, only at major releases](http://lwn.net/Articles/328438/).
+Do not merge in upstream if your code will work and merge cleanly without doing so, Linus even says that [you should never merge in upstream at random points, only at major releases](https://lwn.net/Articles/328438/).
Merging only when needed prevents creating merge commits in your feature branch that later end up littering the master history.
### References
diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md
index f4c4e955d46..1117db98e7e 100644
--- a/doc/workflow/importing/import_projects_from_gitlab_com.md
+++ b/doc/workflow/importing/import_projects_from_gitlab_com.md
@@ -2,12 +2,12 @@
You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if
GitLab support is enabled on your GitLab instance.
-You can read more about Gitlab support [here](http://doc.gitlab.com/ce/integration/gitlab.html)
+You can read more about GitLab support [here](http://doc.gitlab.com/ce/integration/gitlab.html)
To get to the importer page you need to go to "New project" page.
![New project page](gitlab_importer/new_project_page.png)
-Click on the "Import projects from Gitlab.com" link and you will be redirected to GitLab.com
+Click on the "Import projects from GitLab.com" link and you will be redirected to GitLab.com
for permission to access your projects. After accepting, you'll be automatically redirected to the importer.
diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index 485db4834e9..1938ccd0c26 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -6,9 +6,9 @@ Git is a distributed version control system.
There are some major differences between the two, for more information consult your favorite search engine.
Git has tools for migrating SVN repositories to git, namely `git svn`. You can read more about this at
-[git documentation pages](http://git-scm.com/book/en/Git-and-Other-Systems-Git-and-Subversion).
+[git documentation pages](https://git-scm.com/book/en/Git-and-Other-Systems-Git-and-Subversion).
-Apart from the [official git documentation](http://git-scm.com/book/en/Git-and-Other-Systems-Migrating-to-Git) there is also
+Apart from the [official git documentation](https://git-scm.com/book/en/Git-and-Other-Systems-Migrating-to-Git) there is also
user created step by step guide for migrating from SVN to GitLab.
[Benjamin New](https://github.com/leftclickben) wrote [a guide that shows how to do a migration](https://gist.github.com/leftclickben/322b7a3042cbe97ed2af). Mirrors can be found [here](https://gitlab.com/snippets/2168) and [here](https://gist.github.com/maxlazio/f1b593b0d00aa966e9ca).
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
new file mode 100644
index 00000000000..5076b2697a3
--- /dev/null
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -0,0 +1,41 @@
+# GitLab Git LFS Administration
+
+Documentation on how to use Git LFS are under [Managing large binary files with Git LFS doc](manage_large_binaries_with_git_lfs.md).
+
+## Requirements
+
+* Git LFS is supported in GitLab starting with version 8.2.
+* Users need to install [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up.
+
+## Configuration
+
+Git LFS objects can be large in size. By default, they are stored on the server GitLab is installed on.
+
+There are two configuration options to help GitLab server administrators:
+
+* Enabling/disabling Git LFS support
+* Changing the location of LFS object storage
+
+### Omnibus packages
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+gitlab_rails['lfs_enabled'] = false
+gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects"
+```
+
+### Installations from source
+
+In `config/gitlab.yml`:
+
+```yaml
+ lfs:
+ enabled: false
+ storage_path: /mnt/storage/lfs-objects
+```
+
+## Known limitations
+
+* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) is not supported
+* Currently, removing LFS objects from GitLab Git LFS storage is not supported
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
new file mode 100644
index 00000000000..b59e92cb317
--- /dev/null
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -0,0 +1,126 @@
+# Git LFS
+
+Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git.
+The general recommendation is to not have Git repositories larger than 1GB to preserve performance.
+
+GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) (EE only), however in certain
+environments it is not always convenient to use different commands to differentiate between the large files and regular ones.
+
+Git LFS makes this simpler for the end user by removing the requirement to learn new commands.
+
+## How it works
+
+Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication to authorize client requests.
+Once the request is authorized, Git LFS client receives instructions from where to fetch or where to push the large file.
+
+## GitLab server configuration
+
+Documentation for GitLab instance administrators is under [LFS administration doc](lfs_administration.md).
+
+## Requirements
+
+* Git LFS is supported in GitLab starting with version 8.2
+* [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up
+
+## Known limitations
+
+* Git LFS v1 original API is not supported since it was deprecated early in LFS development
+* When SSH is set as a remote, Git LFS objects still go through HTTPS
+* Any Git LFS request will ask for HTTPS credentials to be provided so good Git credentials store is recommended
+* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the URL to Git config manually (see #troubleshooting)
+
+## Using Git LFS
+
+Lets take a look at the workflow when you need to check large files into your Git repository with Git LFS:
+For example, if you want to upload a very large file and check it into your Git repository:
+
+```bash
+git clone git@gitlab.example.com:group/project.git
+git lfs init # initialize the Git LFS project project
+git lfs track "*.iso" # select the file extensions that you want to treat as large files
+```
+
+Once a certain file extension is marked for tracking as a LFS object you can use Git as usual without having to redo the command to track a file with the same extension:
+
+```bash
+cp ~/tmp/debian.iso ./ # copy a large file into the current directory
+git add . # add the large file to the project
+git commit -am "Added Debian iso" # commit the file meta data
+git push origin master # sync the git repo and large file to the GitLab server
+```
+
+Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone command with a SSH URL, you have to enter your GitLab credentials for HTTP authentication.
+
+```bash
+git clone git@gitlab.example.com:group/project.git
+```
+
+If you already cloned the repository and you want to get the latest LFS object that are on the remote repository, eg. from branch `master`:
+
+```bash
+git lfs fetch master
+```
+
+## Troubleshooting
+
+### error: Repository or object not found
+
+There are a couple of reasons why this error can occur:
+
+* You don't have permissions to access certain LFS object
+
+Check if you have permissions to push to the project or fetch from the project.
+
+* Project is not allowed to access the LFS object
+
+LFS object you are trying to push to the project or fetch from the project is not available to the project anymore.
+Probably the object was removed from the server.
+
+* Local git repository is using deprecated LFS API
+
+### Invalid status for <url> : 501
+
+Git LFS will log the failures into a log file.
+To view this log file, while in project directory:
+
+```bash
+git lfs logs last
+```
+
+If the status `error 501` is shown, it is because:
+
+* Git LFS support is not enabled on the GitLab server. Check with your GitLab administrator why Git LFS is not enabled on the server. See [LFS administration documentation](lfs_administration.md) for instructions on how to enable LFS support.
+
+* Git LFS client version is not supported by GitLab server. Check your Git LFS version with `git lfs version`. Check the Git config of the project for traces of deprecated API with `git lfs -l`. If `batch = false` is set in the config, remove the line and try to update your Git LFS client. Only version 1.0.1 and newer are supported.
+
+### getsockopt: connection refused
+
+If you push a LFS object to a project and you receive an error similar to: `Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`,
+the LFS client is trying to reach GitLab through HTTPS. However, your GitLab instance is being served on HTTP.
+
+This behaviour is caused by Git LFS using HTTPS connections by default when a `lfsurl` is not set in the Git config.
+
+To prevent this from happening, set the lfs url in project Git config:
+
+```bash
+
+git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/objects/batch"
+```
+
+### Credentials are always required when pushing an object
+
+Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing the LFS object on every push for every object, user HTTPS credentials are required.
+
+By default, Git has support for remembering the credentials for each repository you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials).
+
+For example, you can tell Git to remember the password for a period of time in which you expect to push the objects:
+
+```bash
+git config --global credential.helper 'cache --timeout=3600'
+```
+
+This will remember the credentials for an hour after which Git operations will require re-authentication.
+
+If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases).
+
+More details about various methods of storing the user credentials can be found on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). \ No newline at end of file
diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md
index 751e19da7f1..6d57b5d98cd 100644
--- a/doc/workflow/merge_requests.md
+++ b/doc/workflow/merge_requests.md
@@ -38,3 +38,15 @@ To check out a particular merge request:
```
$ git checkout origin/merge-requests/1
```
+
+## Ignore whitespace changes in Merge Request diff view
+
+![MR diff](merge_requests/merge_request_diff.png)
+
+It you add `w=1` option to URL, you can see diff without whitespace changes.
+
+![MR diff without whitespace](merge_requests/merge_request_diff_without_whitespace.png)
+
+It is also working on commits compare view.
+
+![Commit Compare](merge_requests/commit_compare.png)
diff --git a/doc/workflow/merge_requests/commit_compare.png b/doc/workflow/merge_requests/commit_compare.png
new file mode 100644
index 00000000000..46b3a56a59b
--- /dev/null
+++ b/doc/workflow/merge_requests/commit_compare.png
Binary files differ
diff --git a/doc/workflow/merge_requests/merge_request_diff.png b/doc/workflow/merge_requests/merge_request_diff.png
new file mode 100644
index 00000000000..ed08ae91bec
--- /dev/null
+++ b/doc/workflow/merge_requests/merge_request_diff.png
Binary files differ
diff --git a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png b/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png
new file mode 100644
index 00000000000..67d67a64d12
--- /dev/null
+++ b/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png
Binary files differ
diff --git a/doc/workflow/milestones.md b/doc/workflow/milestones.md
new file mode 100644
index 00000000000..dff36899aec
--- /dev/null
+++ b/doc/workflow/milestones.md
@@ -0,0 +1,13 @@
+# Milestones
+
+Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
+A common use is keeping track of an upcoming software version. Milestones are created per-project.
+
+![milestone form](milestones/form.png)
+
+## Groups and milestones
+
+You can create a milestone for several projects in the same group simultaneously.
+On the group's milestones page, you will be able to see the status of that milestone across all of the selected projects.
+
+![group milestone form](milestones/group_form.png)
diff --git a/doc/workflow/milestones/form.png b/doc/workflow/milestones/form.png
new file mode 100644
index 00000000000..de44c1ffc1a
--- /dev/null
+++ b/doc/workflow/milestones/form.png
Binary files differ
diff --git a/doc/workflow/milestones/group_form.png b/doc/workflow/milestones/group_form.png
new file mode 100644
index 00000000000..38862dcca68
--- /dev/null
+++ b/doc/workflow/milestones/group_form.png
Binary files differ
diff --git a/doc/workflow/releases.md b/doc/workflow/releases.md
new file mode 100644
index 00000000000..6176784fc57
--- /dev/null
+++ b/doc/workflow/releases.md
@@ -0,0 +1,20 @@
+# Releases
+
+You can turn any git tag into a release, by adding a note to it.
+Release notes behave like any other markdown form in GitLab so you can write text and drag-n-drop files to it.
+Release notes are stored in the database of GitLab.
+
+There are several ways to add release notes:
+
+* In the interface, when you create a new git tag with GitLab
+* In the interface, by adding a note to an existing git tag
+* with the GitLab API
+
+## New tag page with release notes text area
+
+![new_tag](releases/new_tag.png)
+
+## Tags page with button to add or edit release notes for existing git tag
+
+![tags](releases/tags.png)
+
diff --git a/doc/workflow/releases/new_tag.png b/doc/workflow/releases/new_tag.png
new file mode 100644
index 00000000000..e2b64bfe17f
--- /dev/null
+++ b/doc/workflow/releases/new_tag.png
Binary files differ
diff --git a/doc/workflow/releases/tags.png b/doc/workflow/releases/tags.png
new file mode 100644
index 00000000000..aca91906c68
--- /dev/null
+++ b/doc/workflow/releases/tags.png
Binary files differ
diff --git a/doc/workflow/voting_slider.png b/doc/workflow/voting_slider.png
deleted file mode 100644
index 4c660ef9593..00000000000
--- a/doc/workflow/voting_slider.png
+++ /dev/null
Binary files differ
diff --git a/doc_styleguide.md b/doc_styleguide.md
index 656bb1d17ff..cceb449a854 100644
--- a/doc_styleguide.md
+++ b/doc_styleguide.md
@@ -15,6 +15,8 @@ For subtitles, use '##', '###' and so on.
- Do not duplicate information.
- Be brief and clear.
- Whenever it applies, add documents in alphabetical order.
+- Write in US English
+- Use [single spaces](http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html) instead of double spaces.
## Images
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000000..e5a5d8dd53d
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,2 @@
+app:
+ image: gitlab/gitlab-ce:latest
diff --git a/docker/.dockerignore b/docker/.dockerignore
deleted file mode 100644
index dd449725e18..00000000000
--- a/docker/.dockerignore
+++ /dev/null
@@ -1 +0,0 @@
-*.md
diff --git a/docker/Dockerfile b/docker/Dockerfile
deleted file mode 100644
index 304bb97409e..00000000000
--- a/docker/Dockerfile
+++ /dev/null
@@ -1,50 +0,0 @@
-FROM ubuntu:14.04
-MAINTAINER Sytse Sijbrandij
-
-# Install required packages
-RUN apt-get update -q \
- && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
- ca-certificates \
- openssh-server \
- wget \
- apt-transport-https \
- vim \
- nano
-
-# Download & Install GitLab
-# If you run GitLab Enterprise Edition point it to a location where you have downloaded it.
-RUN echo "deb https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/ `lsb_release -cs` main" > /etc/apt/sources.list.d/gitlab_gitlab-ce.list
-RUN wget -q -O - https://packages.gitlab.com/gpg.key | apt-key add -
-RUN apt-get update && apt-get install -yq --no-install-recommends gitlab-ce
-
-# Manage SSHD through runit
-RUN mkdir -p /opt/gitlab/sv/sshd/supervise \
- && mkfifo /opt/gitlab/sv/sshd/supervise/ok \
- && printf "#!/bin/sh\nexec 2>&1\numask 077\nexec /usr/sbin/sshd -D" > /opt/gitlab/sv/sshd/run \
- && chmod a+x /opt/gitlab/sv/sshd/run \
- && ln -s /opt/gitlab/sv/sshd /opt/gitlab/service \
- && mkdir -p /var/run/sshd
-
-# Disabling use DNS in ssh since it tends to slow connecting
-RUN echo "UseDNS no" >> /etc/ssh/sshd_config
-
-# Prepare default configuration
-RUN ( \
- echo "" && \
- echo "# Docker options" && \
- echo "# Prevent Postgres from trying to allocate 25% of total memory" && \
- echo "postgresql['shared_buffers'] = '1MB'" ) >> /etc/gitlab/gitlab.rb && \
- mkdir -p /assets/ && \
- cp /etc/gitlab/gitlab.rb /assets/gitlab.rb
-
-# Expose web & ssh
-EXPOSE 443 80 22
-
-# Define data volumes
-VOLUME ["/etc/gitlab", "/var/opt/gitlab", "/var/log/gitlab"]
-
-# Copy assets
-COPY assets/wrapper /usr/local/bin/
-
-# Wrapper to handle signal, trigger runit and reconfigure GitLab
-CMD ["/usr/local/bin/wrapper"]
diff --git a/docker/README.md b/docker/README.md
index e4d56cdb336..7514d610aec 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -1,169 +1,7 @@
# GitLab Docker images
-The GitLab docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ce/).
-
-## After starting a container
-
-After starting a container you can go to [http://localhost:8080/](http://localhost:8080/) or [http://192.168.59.103:8080/](http://192.168.59.103:8080/) if you use boot2docker.
-
-It might take a while before the docker container is responding to queries.
-
-You can check the status with something like `sudo docker logs -f gitlab`.
-
-You can login to the web interface with username `root` and password `5iveL!fe`.
-
-Next time, you can just use docker start and stop to run the container.
-
-## Run the image
-
-Run the image:
-```bash
-sudo docker run --detach \
- --publish 8443:443 --publish 8080:80 --publish 2222:22 \
- --name gitlab \
- --restart always \
- --volume /srv/gitlab/config:/etc/gitlab \
- --volume /srv/gitlab/logs:/var/log/gitlab \
- --volume /srv/gitlab/data:/var/opt/gitlab \
- gitlab/gitlab-ce:latest
-```
-
-This will download and start GitLab CE container and publish ports needed to access SSH, HTTP and HTTPS.
-All GitLab data will be stored as subdirectories of `/srv/gitlab/`.
-The container will automatically `restart` after system reboot.
-
-After this you can login to the web interface as explained above in 'After starting a container'.
-
-## Where is the data stored?
-
-The GitLab container uses host mounted volumes to store persistent data:
-- `/srv/gitlab/data` mounted as `/var/opt/gitlab` in the container is used for storing *application data*
-- `/srv/gitlab/logs` mounted as `/var/log/gitlab` in the container is used for storing *logs*
-- `/srv/gitlab/config` mounted as `/etc/gitlab` in the container is used for storing *configuration*
-
-You can fine tune these directories to meet your requirements.
-
-### Configure GitLab
-
-This container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`.
-
-To access GitLab configuration, you can start an bash in a new the context of running container, you will be able to browse all directories and use your favorite text editor:
-```bash
-sudo docker exec -it gitlab /bin/bash
-```
-
-You can also edit just `/etc/gitlab/gitlab.rb`:
-```bash
-sudo docker exec -it gitlab vi /etc/gitlab/gitlab.rb
-```
-
-**You should set the `external_url` to point to a valid URL.**
-
-**You may also be interesting in [Enabling HTTPS](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/nginx.md#enable-https).**
-
-**To receive e-mails from GitLab you have to configure the [SMTP settings](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/smtp.md),
-because Docker image doesn't have a SMTP server.**
-
-**Note** that GitLab will reconfigure itself **at each container start.** You will need to restart the container to reconfigure your GitLab:
-
-```bash
-sudo docker restart gitlab
-```
-
-For more options for configuring the container please check [Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration).
-
-## Diagnose potential problems
-
-Read container logs:
-```bash
-sudo docker logs gitlab
-```
-
-Enter running container:
-```bash
-sudo docker exec -it gitlab /bin/bash
-```
-
-From within container you can administrer GitLab container as you would normally administer Omnibus installation: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md.
-
-### Upgrade GitLab to newer version
-
-To upgrade GitLab to new version you have to do:
-1. pull new image,
-```bash
-sudo docker stop gitlab
-```
-
-1. stop running container,
-```bash
-sudo docker rm gitlab
-```
-
-1. remove existing container,
-```bash
-sudo docker pull gitlab/gitlab-ce:latest
-```
-
-1. create the container once again with previously specified options.
-```bash
-sudo docker run --detach \
- --publish 8443:443 --publish 8080:80 --publish 2222:22 \
- --name gitlab \
- --restart always \
- --volume /srv/gitlab/config:/etc/gitlab \
- --volume /srv/gitlab/logs:/var/log/gitlab \
- --volume /srv/gitlab/data:/var/opt/gitlab \
- gitlab/gitlab-ce:latest
-```
-
-On the first run GitLab will reconfigure and update itself.
-
-### Run GitLab CE on public IP address
-
-You can make Docker to use your IP address and forward all traffic to the GitLab CE container.
-You can do that by modifying the `--publish` ([Binding container ports to the host](https://docs.docker.com/articles/networking/#binding-ports)):
-
-> --publish=[] : Publish a container᾿s port or a range of ports to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
-
-To expose GitLab CE on IP 1.1.1.1:
-
-```bash
-sudo docker run --detach \
- --publish 1.1.1.1:443:443 --publish 1.1.1.1:80:80 --publish 1.1.1.1:22:22 \
- --name gitlab \
- --restart always \
- --volume /srv/gitlab/config:/etc/gitlab \
- --volume /srv/gitlab/logs:/var/log/gitlab \
- --volume /srv/gitlab/data:/var/opt/gitlab \
- gitlab/gitlab-ce:latest
-```
-
-You can then access GitLab instance at http://1.1.1.1/ and https://1.1.1.1/.
-
-### Build the image
-
-This guide will also let you know how to build docker image yourself.
-Please run the command from the GitLab repo root directory.
-People using boot2docker should run all the commands without sudo.
-
-```bash
-sudo docker build --tag gitlab/gitlab-ce:latest docker/
-```
-
-### Publish the image to Dockerhub
-
-- Ensure the containers are running
-- Login to Dockerhub with `sudo docker login`
-
-```bash
-sudo docker login
-sudo docker push gitlab/gitlab-ce:latest
-```
-
-## Troubleshooting
-
-Please see the [troubleshooting](troubleshooting.md) file in this directory.
-
-Note: We use `fig.yml` to have compatibility with fig and because docker-compose also supports it.
-
-Our docker image runs chef at every start to generate GitLab configuration.
+* The official GitLab Community Edition Docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ce/).
+* The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ee/).
+* The complete usage guide can be found in [Using GitLab Docker images](http://doc.gitlab.com/omnibus/docker/)
+* The Dockerfile used for building public images is in [Omnibus Repository](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker)
+* Check the guide for [creating Omnibus-based Docker Image](http://doc.gitlab.com/omnibus/build/README.html#Build-Docker-image)
diff --git a/docker/assets/wrapper b/docker/assets/wrapper
deleted file mode 100755
index 8bc8370fbc9..00000000000
--- a/docker/assets/wrapper
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-
-function sigterm_handler() {
- echo "SIGTERM signal received, try to gracefully shutdown all services..."
- gitlab-ctl stop
-}
-
-trap "sigterm_handler; exit" TERM
-
-function entrypoint() {
- /opt/gitlab/embedded/bin/runsvdir-start &
- gitlab-ctl reconfigure # will also start everything
- gitlab-ctl tail # tail all logs
-}
-
-if [[ ! -e /etc/gitlab/gitlab.rb ]]; then
- cp /assets/gitlab.rb /etc/gitlab/gitlab.rb
- chmod 0600 /etc/gitlab/gitlab.rb
-fi
-
-entrypoint
diff --git a/docker/fig.yml b/docker/fig.yml
deleted file mode 100644
index 989551cbfe2..00000000000
--- a/docker/fig.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-app:
- build: .
diff --git a/docker/marathon.json b/docker/marathon.json
deleted file mode 100644
index 9b2091a8c22..00000000000
--- a/docker/marathon.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "id": "/gitlab",
- "ports": [0,0],
- "cpus": 2,
- "mem": 2048.0,
- "disk": 10240.0,
- "container": {
- "type": "DOCKER",
- "docker": {
- "network": "HOST",
- "image": "gitlab/gitlab-ce:latest"
- },
- "volumes": [
- {
- "containerPath": "/etc/gitlab",
- "hostPath": "/var/data/etc/gitlab",
- "mode": "RW"
- },
- {
- "containerPath": "/var/opt/gitlab",
- "hostPath": "/var/data/opt/gitlab",
- "mode": "RW"
- },
- {
- "containerPath": "/var/log/gitlab",
- "hostPath": "/var/data/log/gitlab",
- "mode": "RW"
- }
- ]
- }
-} \ No newline at end of file
diff --git a/docker/troubleshooting.md b/docker/troubleshooting.md
deleted file mode 100644
index 63482547daa..00000000000
--- a/docker/troubleshooting.md
+++ /dev/null
@@ -1,84 +0,0 @@
-# Troubleshooting
-
-This is to troubleshoot https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/245
-But it might contain useful commands for other cases as well.
-
-The configuration to add the postgres log in vim is:
-postgresql['log_directory'] = '/var/log/gitlab/postgresql'
-
-# Commands
-
-```bash
-sudo docker build --tag gitlab/gitlab-ce:latest docker/
-
-sudo docker rm -f gitlab
-
-sudo docker exec -it gitlab vim /etc/gitlab/gitlab.rb
-
-sudo docker exec gitlab tail -f /var/log/gitlab/reconfigure.log
-
-sudo docker exec gitlab tail -f /var/log/gitlab/postgresql/current
-
-sudo docker exec gitlab cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers
-
-sudo docker exec gitlab cat /etc/gitlab/gitlab.rb
-```
-
-# Interactively
-
-```bash
-# First start a GitLab container without starting GitLab
-# This is almost the same as starting the GitLab container except:
-# - we run interactively (-t -i)
-# - we define TERM=linux because it allows to use arrow keys in vi (!!!)
-# - we choose another startup command (bash)
-sudo docker run --ti \
- -e TERM=linux
- --publish 80443:443 --publish 8080:80 --publish 2222:22 \
- --name gitlab \
- --restart always \
- --volume /srv/gitlab/config:/etc/gitlab \
- --volume /srv/gitlab/logs:/var/log/gitlab \
- --volume /srv/gitlab/data:/var/opt/gitlab \
- gitlab/gitlab-ce:latest \
- bash
-
-# Configure GitLab to redirect PostgreSQL logs
-echo "postgresql['log_directory'] = '/var/log/gitlab/postgresql'" >> /etc/gitlab/gitlab.rb
-
-# Prevent Postgres from allocating 25% of total memory
-echo "postgresql['shared_buffers'] = '1MB'" >> /etc/gitlab/gitlab.rb
-
-# You can now start GitLab manually from Bash (in the background)
-# Maybe the command below is still missing something to run in the background
-gitlab-ctl reconfigure > /var/log/gitlab/reconfigure.log & /opt/gitlab/embedded/bin/runsvdir-start &
-
-# Inspect PostgreSQL config
-cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers
-
-# And tail the logs (PostgreSQL log may not exist immediately)
-tail -f /var/log/gitlab/reconfigure.log /var/log/gitlab/postgresql/current
-
-# And get the memory
-cat /proc/meminfo
-head /proc/sys/kernel/shmmax /proc/sys/kernel/shmall
-free -m
-
-```
-
-# Cleanup
-
-Remove ALL docker containers and images (also non GitLab ones).
-**Be careful, because the `-v` also removes volumes attached to the images.**
-
-```bash
-# Remove all containers with attached volumes
-docker rm -v $(docker ps -a -q)
-
-# Remove all images
-docker rmi $(docker images -q)
-
-# Remove GitLab persistent data
-rm -rf /srv/gitlab
-```
-
diff --git a/features/abuse_report.feature b/features/abuse_report.feature
index 3e1cb455b77..212972a762a 100644
--- a/features/abuse_report.feature
+++ b/features/abuse_report.feature
@@ -8,3 +8,10 @@ Feature: Abuse reports
And I click "Report abuse" button
When I fill and submit abuse form
Then I should see success message
+
+ Scenario: Report abuse available only once
+ Given I visit "Mike" user page
+ And I click "Report abuse" button
+ When I fill and submit abuse form
+ And I visit "Mike" user page
+ Then I should see a red "Report abuse" button
diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature
index bbd82a85e3a..76392068357 100644
--- a/features/dashboard/new_project.feature
+++ b/features/dashboard/new_project.feature
@@ -7,24 +7,24 @@ Background:
And I click "New project" link
@javascript
- Scenario: I should see New projects page
- Then I see "New project" page
+ Scenario: I should see New Projects page
+ Then I see "New Project" page
Then I see all possible import optios
@javascript
Scenario: I should see instructions on how to import from Git URL
- Given I see "New project" page
+ Given I see "New Project" page
When I click on "Any repo by URL"
Then I see instructions on how to import from Git URL
@javascript
Scenario: I should see instructions on how to import from GitHub
- Given I see "New project" page
+ Given I see "New Project" page
When I click on "Import project from GitHub"
Then I see instructions on how to import from GitHub
@javascript
Scenario: I should see Google Code import page
- Given I see "New project" page
+ Given I see "New Project" page
When I click on "Google Code"
Then I redirected to Google Code import page
diff --git a/features/group/members.feature b/features/group/members.feature
new file mode 100644
index 00000000000..1f9514bac39
--- /dev/null
+++ b/features/group/members.feature
@@ -0,0 +1,105 @@
+Feature: Group Members
+ Background:
+ Given I sign in as "John Doe"
+ And "John Doe" is owner of group "Owned"
+ And "John Doe" is guest of group "Guest"
+
+ @javascript
+ Scenario: I should add user to group "Owned"
+ Given User "Mary Jane" exists
+ When I visit group "Owned" members page
+ And I select user "Mary Jane" from list with role "Reporter"
+ Then I should see user "Mary Jane" in team list
+
+ @javascript
+ Scenario: Add user to group
+ Given gitlab user "Mike"
+ When I visit group "Owned" members page
+ When I select "Mike" as "Reporter"
+ Then I should see "Mike" in team list as "Reporter"
+
+ @javascript
+ Scenario: Ignore add user to group when is already Owner
+ Given gitlab user "Mike"
+ When I visit group "Owned" members page
+ When I select "Mike" as "Reporter"
+ Then I should see "Mike" in team list as "Owner"
+
+ @javascript
+ Scenario: Invite user to group
+ When I visit group "Owned" members page
+ When I select "sjobs@apple.com" as "Reporter"
+ Then I should see "sjobs@apple.com" in team list as invited "Reporter"
+
+ @javascript
+ Scenario: Edit group member permissions
+ Given "Mary Jane" is guest of group "Owned"
+ And I visit group "Owned" members page
+ When I change the "Mary Jane" role to "Developer"
+ Then I should see "Mary Jane" as "Developer"
+
+ # Leave
+
+ @javascript
+ Scenario: Owner should be able to remove himself from group if he is not the last owner
+ Given "Mary Jane" is owner of group "Owned"
+ When I visit group "Owned" members page
+ Then I should see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+ When I click on the "Remove User From Group" button for "John Doe"
+ And I visit group "Owned" members page
+ Then I should not see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+
+ @javascript
+ Scenario: Owner should not be able to remove himself from group if he is the last owner
+ Given "Mary Jane" is guest of group "Owned"
+ When I visit group "Owned" members page
+ Then I should see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+ Then I should not see the "Remove User From Group" button for "John Doe"
+
+ @javascript
+ Scenario: Guest should be able to remove himself from group
+ Given "Mary Jane" is guest of group "Guest"
+ When I visit group "Guest" members page
+ Then I should see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+ When I click on the "Remove User From Group" button for "John Doe"
+ When I visit group "Guest" members page
+ Then I should not see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+
+ @javascript
+ Scenario: Guest should be able to remove himself from group even if he is the only user in the group
+ When I visit group "Guest" members page
+ Then I should see user "John Doe" in team list
+ When I click on the "Remove User From Group" button for "John Doe"
+ When I visit group "Guest" members page
+ Then I should not see user "John Doe" in team list
+
+ # Remove others
+
+ Scenario: Owner should be able to remove other users from group
+ Given "Mary Jane" is owner of group "Owned"
+ When I visit group "Owned" members page
+ Then I should see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+ When I click on the "Remove User From Group" button for "Mary Jane"
+ When I visit group "Owned" members page
+ Then I should see user "John Doe" in team list
+ Then I should not see user "Mary Jane" in team list
+
+ Scenario: Guest should not be able to remove other users from group
+ Given "Mary Jane" is guest of group "Guest"
+ When I visit group "Guest" members page
+ Then I should see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+ Then I should not see the "Remove User From Group" button for "Mary Jane"
+
+ Scenario: Search member by name
+ Given "Mary Jane" is guest of group "Guest"
+ And I visit group "Guest" members page
+ When I search for 'Mary' member
+ Then I should see user "Mary Jane" in team list
+ Then I should not see user "John Doe" in team list
diff --git a/features/group/milestones.feature b/features/group/milestones.feature
new file mode 100644
index 00000000000..62ea66a783c
--- /dev/null
+++ b/features/group/milestones.feature
@@ -0,0 +1,30 @@
+Feature: Group Milestones
+ Background:
+ Given I sign in as "John Doe"
+ And "John Doe" is owner of group "Owned"
+
+ Scenario: I should see group "Owned" milestone index page with no milestones
+ When I visit group "Owned" page
+ And I click on group milestones
+ Then I should see group milestones index page has no milestones
+
+ Scenario: I should see group "Owned" milestone index page with milestones
+ Given Group has projects with milestones
+ When I visit group "Owned" page
+ And I click on group milestones
+ Then I should see group milestones index page with milestones
+
+ Scenario: I should see group "Owned" milestone show page
+ Given Group has projects with milestones
+ When I visit group "Owned" page
+ And I click on group milestones
+ And I click on one group milestone
+ Then I should see group milestone with descriptions and expiry date
+ And I should see group milestone with all issues and MRs assigned to that milestone
+
+ Scenario: Create multiple milestones with one form
+ Given I visit group "Owned" milestones page
+ And I click new milestone button
+ And I fill milestone name
+ When I press create mileston button
+ Then milestone in each project should be created
diff --git a/features/groups.feature b/features/groups.feature
index db37fa3b375..c803e952980 100644
--- a/features/groups.feature
+++ b/features/groups.feature
@@ -2,7 +2,6 @@ Feature: Groups
Background:
Given I sign in as "John Doe"
And "John Doe" is owner of group "Owned"
- And "John Doe" is guest of group "Guest"
Scenario: I should have back to group button
When I visit group "Owned" page
@@ -24,13 +23,6 @@ Feature: Groups
When I visit group "Owned" merge requests page
Then I should see merge requests from group "Owned" assigned to me
- @javascript
- Scenario: I should add user to projects in group "Owned"
- Given User "Mary Jane" exists
- When I visit group "Owned" members page
- And I select user "Mary Jane" from list with role "Reporter"
- Then I should see user "Mary Jane" in team list
-
Scenario: I should see edit group "Owned" page
When I visit group "Owned" settings page
And I change group "Owned" name to "new-name"
@@ -51,108 +43,6 @@ Feature: Groups
Then I should not see group "Owned" avatar
And I should not see the "Remove avatar" button
- @javascript
- Scenario: Add user to group
- Given gitlab user "Mike"
- When I visit group "Owned" members page
- And I click link "Add members"
- When I select "Mike" as "Reporter"
- Then I should see "Mike" in team list as "Reporter"
-
- @javascript
- Scenario: Invite user to group
- When I visit group "Owned" members page
- And I click link "Add members"
- When I select "sjobs@apple.com" as "Reporter"
- Then I should see "sjobs@apple.com" in team list as invited "Reporter"
-
- # Leave
-
- @javascript
- Scenario: Owner should be able to remove himself from group if he is not the last owner
- Given "Mary Jane" is owner of group "Owned"
- When I visit group "Owned" members page
- Then I should see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
- When I click on the "Remove User From Group" button for "John Doe"
- And I visit group "Owned" members page
- Then I should not see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
-
- @javascript
- Scenario: Owner should not be able to remove himself from group if he is the last owner
- Given "Mary Jane" is guest of group "Owned"
- When I visit group "Owned" members page
- Then I should see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
- Then I should not see the "Remove User From Group" button for "John Doe"
-
- @javascript
- Scenario: Guest should be able to remove himself from group
- Given "Mary Jane" is guest of group "Guest"
- When I visit group "Guest" members page
- Then I should see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
- When I click on the "Remove User From Group" button for "John Doe"
- When I visit group "Guest" members page
- Then I should not see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
-
- @javascript
- Scenario: Guest should be able to remove himself from group even if he is the only user in the group
- When I visit group "Guest" members page
- Then I should see user "John Doe" in team list
- When I click on the "Remove User From Group" button for "John Doe"
- When I visit group "Guest" members page
- Then I should not see user "John Doe" in team list
-
- # Remove others
-
- Scenario: Owner should be able to remove other users from group
- Given "Mary Jane" is owner of group "Owned"
- When I visit group "Owned" members page
- Then I should see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
- When I click on the "Remove User From Group" button for "Mary Jane"
- When I visit group "Owned" members page
- Then I should see user "John Doe" in team list
- Then I should not see user "Mary Jane" in team list
-
- Scenario: Guest should not be able to remove other users from group
- Given "Mary Jane" is guest of group "Guest"
- When I visit group "Guest" members page
- Then I should see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
- Then I should not see the "Remove User From Group" button for "Mary Jane"
-
- Scenario: Search member by name
- Given "Mary Jane" is guest of group "Guest"
- And I visit group "Guest" members page
- When I search for 'Mary' member
- Then I should see user "Mary Jane" in team list
- Then I should not see user "John Doe" in team list
-
- # Group milestones
-
- Scenario: I should see group "Owned" milestone index page with no milestones
- When I visit group "Owned" page
- And I click on group milestones
- Then I should see group milestones index page has no milestones
-
- Scenario: I should see group "Owned" milestone index page with milestones
- Given Group has projects with milestones
- When I visit group "Owned" page
- And I click on group milestones
- Then I should see group milestones index page with milestones
-
- Scenario: I should see group "Owned" milestone show page
- Given Group has projects with milestones
- When I visit group "Owned" page
- And I click on group milestones
- And I click on one group milestone
- Then I should see group milestone with descriptions and expiry date
- And I should see group milestone with all issues and MRs assigned to that milestone
-
# Group projects in settings
Scenario: I should see all projects in the project list in settings
Given Group "Owned" has archived project
@@ -169,4 +59,4 @@ Feature: Groups
When I visit group "Owned" page
Then I should see group "Owned"
Then I should see project "Public-project"
-
+
diff --git a/features/profile/profile.feature b/features/profile/profile.feature
index 27c0bde364e..168d9d30b50 100644
--- a/features/profile/profile.feature
+++ b/features/profile/profile.feature
@@ -7,6 +7,7 @@ Feature: Profile
Given I visit profile page
Then I should see my profile info
+ @javascript
Scenario: I can see groups I belong to
Given I have group with projects
When I visit profile page
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index 34161b81d44..e4beeb59adc 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -20,6 +20,8 @@ Feature: Project Commits
Given commit has ci status
And I click on commit link
Then I see commit ci info
+ And I click status link
+ Then I see builds list
Scenario: I browse commit with side-by-side diff view
Given I click on commit link
diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature
index 4a2b870e082..d6e0c84537e 100644
--- a/features/project/commits/diff_comments.feature
+++ b/features/project/commits/diff_comments.feature
@@ -14,6 +14,12 @@ Feature: Project Commits Diff Comments
Then I should see a diff comment saying "Typo, please fix"
@javascript
+ Scenario: I can add a diff comment with a single emoji
+ Given I open a diff comment form
+ And I write a diff comment like ":smile:"
+ Then I should see a diff comment with an emoji image
+
+ @javascript
Scenario: I get a temporary form for the first comment on a diff line
Given I open a diff comment form
Then I should see a temporary diff comment form
diff --git a/features/project/commits/tags.feature b/features/project/commits/tags.feature
index 02f399f7cad..56ee091acc0 100644
--- a/features/project/commits/tags.feature
+++ b/features/project/commits/tags.feature
@@ -12,6 +12,12 @@ Feature: Project Commits Tags
And I submit new tag form
Then I should see new tag created
+ Scenario: I create a tag with release notes
+ Given I click new tag link
+ And I submit new tag form with release notes
+ Then I should see new tag created
+ And I should see tag release notes
+
Scenario: I create a tag with invalid name
And I click new tag link
And I submit new tag form with invalid name
@@ -27,15 +33,13 @@ Feature: Project Commits Tags
And I submit new tag form with tag that already exists
Then I should see new an error that tag already exists
- @javascript
Scenario: I delete a tag
+ Given I visit tag 'v1.1.0' page
Given I delete tag 'v1.1.0'
Then I should not see tag 'v1.1.0'
- @javascript
- Scenario: I delete all tags and see info message
- Given I delete all tags
- Then I should see tags info message
-
- # @wip
- # Scenario: I can download project by tag
+ Scenario: I add release notes to the tag
+ Given I visit tag 'v1.1.0' page
+ When I click edit tag link
+ And I fill release notes and submit form
+ Then I should see tag release notes
diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature
new file mode 100644
index 00000000000..2609f129d07
--- /dev/null
+++ b/features/project/issues/award_emoji.feature
@@ -0,0 +1,18 @@
+Feature: Award Emoji
+ Background:
+ Given I sign in as a user
+ And I own project "Shop"
+ And project "Shop" has issue "Bugfix"
+ And I visit "Bugfix" issue page
+
+ @javascript
+ Scenario: I add and remove award in the issue
+ Given I click to emoji-picker
+ And I click to emoji in the picker
+ Then I have award added
+ And I can remove it by clicking to icon
+
+ @javascript
+ Scenario: I add award emoji using regular comment
+ Given I leave comment with a single emoji
+ Then I have award added
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 947f668e432..6cd081c868e 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -10,6 +10,21 @@ Feature: Project Merge Requests
Then I should see "Bug NS-04" in merge requests
And I should not see "Feature NS-03" in merge requests
+ Scenario: I should see CI status for merge requests
+ Given project "Shop" have "Bug NS-05" open merge request with diffs inside
+ Given "Bug NS-05" has CI status
+ When I visit project "Shop" merge requests page
+ Then I should see merge request "Bug NS-05" with CI status
+
+ Scenario: I should not see target branch name when it is project's default branch
+ Then I should see "Bug NS-04" in merge requests
+ And I should not see "master" branch
+
+ Scenario: I should see target branch when it is different from default
+ Given project "Shop" have "Bug NS-06" open merge request
+ When I visit project "Shop" merge requests page
+ Then I should see "other_branch" branch
+
Scenario: I should see rejected merge requests
Given I click link "Closed"
Then I should see "Feature NS-03" in merge requests
@@ -115,40 +130,40 @@ Feature: Project Merge Requests
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
- And I leave a comment like "Line is wrong" on line 39 of the second file
- And I click link "Hide inline discussion" of the second file
- Then I should not see a comment like "Line is wrong here" in the second file
+ And I leave a comment like "Line is wrong" on line 39 of the third file
+ And I click link "Hide inline discussion" of the third file
+ Then I should not see a comment like "Line is wrong here" in the third file
@javascript
Scenario: I show comments on a merge request diff with comments in a single file
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
- And I leave a comment like "Line is wrong" on line 39 of the second file
- Then I should see a comment like "Line is wrong" in the second file
+ And I leave a comment like "Line is wrong" on line 39 of the third file
+ Then I should see a comment like "Line is wrong" in the third file
@javascript
Scenario: I hide comments on a merge request diff with comments in multiple files
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
- And I leave a comment like "Line is correct" on line 12 of the first file
- And I leave a comment like "Line is wrong" on line 39 of the second file
- And I click link "Hide inline discussion" of the second file
- Then I should not see a comment like "Line is wrong here" in the second file
- And I should still see a comment like "Line is correct" in the first file
+ And I leave a comment like "Line is correct" on line 12 of the second file
+ And I leave a comment like "Line is wrong" on line 39 of the third file
+ And I click link "Hide inline discussion" of the third file
+ Then I should not see a comment like "Line is wrong here" in the third file
+ And I should still see a comment like "Line is correct" in the second file
@javascript
Scenario: I show comments on a merge request diff with comments in multiple files
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
- And I leave a comment like "Line is correct" on line 12 of the first file
- And I leave a comment like "Line is wrong" on line 39 of the second file
- And I click link "Hide inline discussion" of the second file
- And I click link "Show inline discussion" of the second file
- Then I should see a comment like "Line is wrong" in the second file
- And I should still see a comment like "Line is correct" in the first file
+ And I leave a comment like "Line is correct" on line 12 of the second file
+ And I leave a comment like "Line is wrong" on line 39 of the third file
+ And I click link "Hide inline discussion" of the third file
+ And I click link "Show inline discussion" of the third file
+ Then I should see a comment like "Line is wrong" in the third file
+ And I should still see a comment like "Line is correct" in the second file
@javascript
Scenario: I unfold diff
@@ -163,8 +178,8 @@ Feature: Project Merge Requests
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
- And I leave a comment like "Line is correct" on line 12 of the first file
- And I leave a comment like "Line is wrong" on line 39 of the second file
+ And I leave a comment like "Line is correct" on line 12 of the second file
+ And I leave a comment like "Line is wrong" on line 39 of the third file
And I click Side-by-side Diff tab
Then I should see comments on the side-by-side diff page
diff --git a/features/project/merge_requests/accept.feature b/features/project/merge_requests/accept.feature
new file mode 100644
index 00000000000..9bc2b7c8eca
--- /dev/null
+++ b/features/project/merge_requests/accept.feature
@@ -0,0 +1,19 @@
+Feature: Project Merge Requests Acceptance
+ Background:
+ Given There is an open Merge Request
+ And I am signed in as a developer of the project
+
+ @javascript
+ Scenario: Accepting the Merge Request and removing the source branch
+ Given I am on the Merge Request detail page
+ When I click on "Remove source branch" option
+ And I click on Accept Merge Request
+ Then I should see merge request merged
+ And I should not see the Remove Source Branch button
+
+ @javascript
+ Scenario: Accepting the Merge Request without removing the source branch
+ Given I am on the Merge Request detail page
+ When I click on Accept Merge Request
+ Then I should see merge request merged
+ And I should see the Remove Source Branch button
diff --git a/features/project/project.feature b/features/project/project.feature
index b3fb0794547..1a53945eb04 100644
--- a/features/project/project.feature
+++ b/features/project/project.feature
@@ -31,6 +31,12 @@ Feature: Project
And I visit project "Shop" page
Then I should see project "Shop" README
+ Scenario: I should see last commit with CI
+ Given project "Shop" has CI enabled
+ Given project "Shop" has CI build
+ And I visit project "Shop" page
+ And I should see last commit with CI status
+
@javascript
Scenario: I should see project activity
When I visit project "Shop" activity page
diff --git a/features/project/service.feature b/features/project/service.feature
index fdff640ec85..5014b52b9f6 100644
--- a/features/project/service.feature
+++ b/features/project/service.feature
@@ -72,6 +72,7 @@ Feature: Project Services
And I click Atlassian Bamboo CI service link
And I fill Atlassian Bamboo CI settings
Then I should see Atlassian Bamboo CI service settings saved
+ And I should see empty field Change Password
Scenario: Activate jetBrains TeamCity CI service
When I visit project "Shop" services page
diff --git a/features/project/snippets.feature b/features/project/snippets.feature
index 77e42a1a38b..270557cbde7 100644
--- a/features/project/snippets.feature
+++ b/features/project/snippets.feature
@@ -30,5 +30,5 @@ Feature: Project Snippets
Scenario: I destroy "Snippet one"
Given I visit snippet page "Snippet one"
- And I click link "Remove Snippet"
+ And I click link "Delete"
Then I should not see "Snippet one" in snippets
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index 58574166ef3..e545ea63ca8 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -21,12 +21,12 @@ Feature: Project Source Browse Files
Then I should see raw file content
Scenario: I can create file
- Given I click on "new file" link in repo
+ Given I click on "New file" link in repo
Then I can see new file page
@javascript
Scenario: I can create and commit file
- Given I click on "new file" link in repo
+ Given I click on "New file" link in repo
And I edit code
And I fill the new file name
And I fill the commit message
@@ -36,14 +36,13 @@ Feature: Project Source Browse Files
@javascript
Scenario: I can upload file and commit
- Given I click on "new file" link in repo
- Then I can see new file page
- And I can see "upload an existing one"
- And I click on "upload"
+ Given I click on "Upload file" link in repo
And I upload a new text file
And I fill the upload file commit message
+ And I fill the new branch name
And I click on "Upload file"
Then I can see the new text file
+ And I am redirected to the new merge request page
And I can see the new commit message
@javascript
@@ -59,13 +58,13 @@ Feature: Project Source Browse Files
@javascript
Scenario: I can create and commit file and specify new branch
- Given I click on "new file" link in repo
+ Given I click on "New file" link in repo
And I edit code
And I fill the new file name
And I fill the commit message
And I fill the new branch name
And I click on "Commit Changes"
- Then I am redirected to the new file on new branch
+ Then I am redirected to the new merge request page
And I should see its new content
@javascript
@@ -83,7 +82,7 @@ Feature: Project Source Browse Files
@javascript
Scenario: If I enter an illegal file name I see an error message
- Given I click on "new file" link in repo
+ Given I click on "New file" link in repo
And I fill the new file name with an illegal name
And I edit code
And I fill the commit message
@@ -92,6 +91,16 @@ Feature: Project Source Browse Files
And I see a commit error message
@javascript
+ Scenario: I can create file with a directory name
+ Given I click on "New file" link in repo
+ And I fill the new file name with a new directory
+ And I edit code
+ And I fill the commit message
+ And I click on "Commit changes"
+ Then I am redirected to the new file with directory
+ And I should see its new content
+
+ @javascript
Scenario: I can edit file
Given I click on ".gitignore" file in repo
And I click button "Edit"
@@ -125,7 +134,7 @@ Feature: Project Source Browse Files
And I fill the commit message
And I fill the new branch name
And I click on "Commit Changes"
- Then I am redirected to the ".gitignore" on new branch
+ Then I am redirected to the new merge request page
And I should see its new content
@javascript @wip
@@ -139,6 +148,24 @@ Feature: Project Source Browse Files
And I see a commit error message
@javascript
+ Scenario: I can create directory in repo
+ When I click on "New directory" link in repo
+ And I fill the new directory name
+ And I fill the commit message
+ And I fill the new branch name
+ And I click on "Create directory"
+ Then I am redirected to the new merge request page
+
+ @javascript
+ Scenario: I attempt to create an existing directory
+ When I click on "New directory" link in repo
+ And I fill an existing directory name
+ And I fill the commit message
+ And I click on "Create directory"
+ Then I see "Unable to create directory"
+ And I am redirected to the root directory
+
+ @javascript
Scenario: I can see editing preview
Given I click on ".gitignore" file in repo
And I click button "Edit"
@@ -147,12 +174,12 @@ Feature: Project Source Browse Files
Then I see diff
@javascript
- Scenario: I can remove file and commit
+ Scenario: I can delete file and commit
Given I click on ".gitignore" file in repo
And I see the ".gitignore"
- And I click on "Remove"
+ And I click on "Delete"
And I fill the commit message
- And I click on "Remove file"
+ And I click on "Delete file"
Then I am redirected to the files URL
And I don't see the ".gitignore"
@@ -188,3 +215,9 @@ Feature: Project Source Browse Files
And I see the ref 'test' has been selected
And I visit the 'test' tree
Then I see the commit data
+
+ @javascript
+ Scenario: I browse code with a leading dot in the directory
+ Given I switch ref to fix
+ And I visit the fix tree
+ Then I see the commit data for a directory with a leading dot
diff --git a/features/project/team_management.feature b/features/project/team_management.feature
index 09a7df59df6..06fb45c8bde 100644
--- a/features/project/team_management.feature
+++ b/features/project/team_management.feature
@@ -13,14 +13,12 @@ Feature: Project Team Management
@javascript
Scenario: Add user to project
- Given I click link "Add members"
- And I select "Mike" as "Reporter"
+ When I select "Mike" as "Reporter"
Then I should see "Mike" in team list as "Reporter"
@javascript
Scenario: Invite user to project
- Given I click link "Add members"
- And I select "sjobs@apple.com" as "Reporter"
+ When I select "sjobs@apple.com" as "Reporter"
Then I should see "sjobs@apple.com" in team list as invited "Reporter"
@javascript
diff --git a/features/snippets/snippets.feature b/features/snippets/snippets.feature
index 4f617b6bed8..e15d7c79342 100644
--- a/features/snippets/snippets.feature
+++ b/features/snippets/snippets.feature
@@ -24,7 +24,7 @@ Feature: Snippets
Scenario: I destroy "Personal snippet one"
Given I visit snippet page "Personal snippet one"
- And I click link "Destroy"
+ And I click link "Delete"
Then I should not see "Personal snippet one" in snippets
Scenario: I create new internal snippet
diff --git a/features/steps/abuse_reports.rb b/features/steps/abuse_reports.rb
index 8f9ddb2899f..499accb0b08 100644
--- a/features/steps/abuse_reports.rb
+++ b/features/steps/abuse_reports.rb
@@ -22,6 +22,10 @@ class Spinach::Features::AbuseReports < Spinach::FeatureSteps
user_mike
end
+ step 'I should see a red "Report abuse" button' do
+ expect(page).to have_button("Already reported for abuse")
+ end
+
def user_mike
@user_mike ||= create(:user, name: 'Mike')
end
diff --git a/features/steps/admin/labels.rb b/features/steps/admin/labels.rb
index d64380abf73..55ddcc25085 100644
--- a/features/steps/admin/labels.rb
+++ b/features/steps/admin/labels.rb
@@ -17,7 +17,7 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps
step 'I remove label \'bug\'' do
page.within "#label_#{bug_label.id}" do
- click_link 'Remove'
+ click_link 'Delete'
end
end
@@ -38,28 +38,28 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps
step 'I should see labels help message' do
page.within '.labels' do
- expect(page).to have_content 'There are no any labels yet'
+ expect(page).to have_content 'There are no labels yet'
end
end
step 'I submit new label \'support\'' do
visit new_admin_label_path
fill_in 'Title', with: 'support'
- fill_in 'Background Color', with: '#F95610'
+ fill_in 'Background color', with: '#F95610'
click_button 'Save'
end
step 'I submit new label \'bug\'' do
visit new_admin_label_path
fill_in 'Title', with: 'bug'
- fill_in 'Background Color', with: '#F95610'
+ fill_in 'Background color', with: '#F95610'
click_button 'Save'
end
step 'I submit new label with invalid color' do
visit new_admin_label_path
fill_in 'Title', with: 'support'
- fill_in 'Background Color', with: '#12'
+ fill_in 'Background color', with: '#12'
click_button 'Save'
end
@@ -71,7 +71,7 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps
step 'I should see label color error message' do
page.within '.label-form' do
- expect(page).to have_content 'Color is invalid'
+ expect(page).to have_content 'Color must be a valid color code'
end
end
@@ -101,7 +101,7 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps
step 'I change label \'bug\' to \'fix\'' do
fill_in 'Title', with: 'fix'
- fill_in 'Background Color', with: '#F15610'
+ fill_in 'Background color', with: '#F15610'
click_button 'Save'
end
diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb
index 17233f89f38..5a1cc9aa151 100644
--- a/features/steps/admin/projects.rb
+++ b/features/steps/admin/projects.rb
@@ -41,6 +41,8 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps
end
step 'I transfer project to group \'Web\'' do
+ allow_any_instance_of(Projects::TransferService).
+ to receive(:move_uploads_to_new_namespace).and_return(true)
find(:xpath, "//input[@id='new_namespace_id']").set group.id
click_button 'Transfer'
end
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index 1e09162a5b5..a0aad66184d 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -3,13 +3,13 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
include SharedPaths
include SharedProject
- step 'I click "New project" link' do
+ step 'I click "New Project" link' do
page.within('.content') do
- click_link "New project"
+ click_link "New Project"
end
end
- step 'I see "New project" page' do
+ step 'I see "New Project" page' do
expect(page).to have_content('Project path')
end
@@ -44,7 +44,6 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
git_import_instructions = first('.js-toggle-content')
expect(git_import_instructions).to be_visible
expect(git_import_instructions).to have_content "Git repository URL"
- expect(git_import_instructions).to have_content "The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL:"
end
step 'I click on "Google Code"' do
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
new file mode 100644
index 00000000000..0706df3aec5
--- /dev/null
+++ b/features/steps/group/members.rb
@@ -0,0 +1,147 @@
+class Spinach::Features::GroupMembers < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedGroup
+ include SharedUser
+ include Select2Helper
+
+ step 'I select "Mike" as "Reporter"' do
+ user = User.find_by(name: "Mike")
+
+ page.within ".users-group-form" do
+ select2(user.id, from: "#user_ids", multiple: true)
+ select "Reporter", from: "access_level"
+ end
+
+ click_button "Add users to group"
+ end
+
+ step 'I select "Mike" as "Master"' do
+ user = User.find_by(name: "Mike")
+
+ page.within ".users-group-form" do
+ select2(user.id, from: "#user_ids", multiple: true)
+ select "Master", from: "access_level"
+ end
+
+ click_button "Add users to group"
+ end
+
+ step 'I should see "Mike" in team list as "Reporter"' do
+ page.within '.content-list' do
+ expect(page).to have_content('Mike')
+ expect(page).to have_content('Reporter')
+ end
+ end
+
+ step 'I should see "Mike" in team list as "Owner"' do
+ page.within '.content-list' do
+ expect(page).to have_content('Mike')
+ expect(page).to have_content('Owner')
+ end
+ end
+
+ step 'I select "sjobs@apple.com" as "Reporter"' do
+ page.within ".users-group-form" do
+ select2("sjobs@apple.com", from: "#user_ids", multiple: true)
+ select "Reporter", from: "access_level"
+ end
+
+ click_button "Add users to group"
+ end
+
+ step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
+ page.within '.content-list' do
+ expect(page).to have_content('sjobs@apple.com')
+ expect(page).to have_content('invited')
+ expect(page).to have_content('Reporter')
+ end
+ end
+
+ step 'I select user "Mary Jane" from list with role "Reporter"' do
+ user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane")
+
+ page.within ".users-group-form" do
+ select2(user.id, from: "#user_ids", multiple: true)
+ select "Reporter", from: "access_level"
+ end
+
+ click_button "Add users to group"
+ end
+
+ step 'I should see user "John Doe" in team list' do
+ expect(group_members_list).to have_content("John Doe")
+ end
+
+ step 'I should not see user "John Doe" in team list' do
+ expect(group_members_list).not_to have_content("John Doe")
+ end
+
+ step 'I should see user "Mary Jane" in team list' do
+ expect(group_members_list).to have_content("Mary Jane")
+ end
+
+ step 'I should not see user "Mary Jane" in team list' do
+ expect(group_members_list).not_to have_content("Mary Jane")
+ end
+
+ step 'I click on the "Remove User From Group" button for "John Doe"' do
+ find(:css, 'li', text: "John Doe").find(:css, 'a.btn-remove').click
+ # poltergeist always confirms popups.
+ end
+
+ step 'I click on the "Remove User From Group" button for "Mary Jane"' do
+ find(:css, 'li', text: "Mary Jane").find(:css, 'a.btn-remove').click
+ # poltergeist always confirms popups.
+ end
+
+ step 'I should not see the "Remove User From Group" button for "John Doe"' do
+ expect(find(:css, 'li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove')
+ # poltergeist always confirms popups.
+ end
+
+ step 'I should not see the "Remove User From Group" button for "Mary Jane"' do
+ expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove')
+ # poltergeist always confirms popups.
+ end
+
+ step 'I search for \'Mary\' member' do
+ page.within '.member-search-form' do
+ fill_in 'search', with: 'Mary'
+ click_button 'Search'
+ end
+ end
+
+ step 'I change the "Mary Jane" role to "Developer"' do
+ member = mary_jane_member
+
+ page.within "#group_member_#{member.id}" do
+ find(".js-toggle-button").click
+ page.within "#edit_group_member_#{member.id}" do
+ select 'Developer', from: 'group_member_access_level'
+ click_on 'Save'
+ end
+ end
+ end
+
+ step 'I should see "Mary Jane" as "Developer"' do
+ member = mary_jane_member
+
+ page.within "#group_member_#{member.id}" do
+ page.within '.member-access-level' do
+ expect(page).to have_content "Developer"
+ end
+ end
+ end
+
+ private
+
+ def mary_jane_member
+ user = User.find_by(name: "Mary Jane")
+ owned_group.members.find_by(user_id: user.id)
+ end
+
+ def group_members_list
+ find(".panel .content-list")
+ end
+end
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
new file mode 100644
index 00000000000..6e57b16ccb6
--- /dev/null
+++ b/features/steps/group/milestones.rb
@@ -0,0 +1,90 @@
+class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedGroup
+ include SharedUser
+
+ step 'I click on group milestones' do
+ click_link 'Milestones'
+ end
+
+ step 'I should see group milestones index page has no milestones' do
+ expect(page).to have_content('No milestones to show')
+ end
+
+ step 'Group has projects with milestones' do
+ group_milestone
+ end
+
+ step 'I should see group milestones index page with milestones' do
+ expect(page).to have_content('Version 7.2')
+ expect(page).to have_content('GL-113')
+ expect(page).to have_link('3 Issues', href: issues_group_path("owned", milestone_title: "Version 7.2"))
+ expect(page).to have_link('0 Merge Requests', href: merge_requests_group_path("owned", milestone_title: "GL-113"))
+ end
+
+ step 'I click on one group milestone' do
+ click_link 'GL-113'
+ end
+
+ step 'I should see group milestone with descriptions and expiry date' do
+ expect(page).to have_content('expires at Aug 20, 2114')
+ end
+
+ step 'I should see group milestone with all issues and MRs assigned to that milestone' do
+ expect(page).to have_content('Milestone GL-113')
+ expect(page).to have_content('Progress: 0 closed – 3 open')
+ issue = Milestone.find_by(name: 'GL-113').issues.first
+ expect(page).to have_link(issue.title, href: namespace_project_issue_path(issue.project.namespace, issue.project, issue))
+ end
+
+ step 'I fill milestone name' do
+ fill_in 'milestone_title', with: 'v2.9.0'
+ end
+
+ step 'I click new milestone button' do
+ click_link "New Milestone"
+ end
+
+ step 'I press create mileston button' do
+ click_button "Create Milestone"
+ end
+
+ step 'milestone in each project should be created' do
+ group = Group.find_by(name: 'Owned')
+ expect(page).to have_content "Milestone v2.9.0"
+ expect(group.projects).to be_present
+
+ group.projects.each do |project|
+ expect(page).to have_content project.name
+ end
+ end
+
+ private
+
+ def group_milestone
+ group = owned_group
+
+ %w(gitlabhq gitlab-ci cookbook-gitlab).each do |path|
+ project = create :project, path: path, group: group
+ milestone = create :milestone, title: "Version 7.2", project: project
+ create :issue,
+ project: project,
+ assignee: current_user,
+ author: current_user,
+ milestone: milestone
+
+ milestone = create :milestone,
+ title: "GL-113",
+ project: project,
+ due_date: '2114-08-20',
+ description: 'Lorem Ipsum is simply dummy text'
+
+ create :issue,
+ project: project,
+ assignee: current_user,
+ author: current_user,
+ milestone: milestone
+ end
+ end
+end
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 95bc9baf8d8..f5e3fee61c0 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -3,18 +3,9 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
include SharedPaths
include SharedGroup
include SharedUser
- include Select2Helper
step 'I should see back to dashboard button' do
- expect(page).to have_content 'Back to dashboard'
- end
-
- step 'gitlab user "Mike"' do
- create(:user, name: "Mike")
- end
-
- step 'I click link "Add members"' do
- find(:css, 'button.btn-new').click
+ expect(page).to have_content 'Go to dashboard'
end
step 'I should see group "Owned"' do
@@ -26,7 +17,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
end
step 'Group "Owned" has a public project "Public-project"' do
- group = Group.find_by(name: "Owned")
+ group = owned_group
@project = create :empty_project, :public,
group: group,
@@ -37,43 +28,8 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
expect(page).to have_content 'Public-project'
end
- step 'I select "Mike" as "Reporter"' do
- user = User.find_by(name: "Mike")
-
- page.within ".users-group-form" do
- select2(user.id, from: "#user_ids", multiple: true)
- select "Reporter", from: "access_level"
- end
-
- click_button "Add users to group"
- end
-
- step 'I should see "Mike" in team list as "Reporter"' do
- page.within '.well-list' do
- expect(page).to have_content('Mike')
- expect(page).to have_content('Reporter')
- end
- end
-
- step 'I select "sjobs@apple.com" as "Reporter"' do
- page.within ".users-group-form" do
- select2("sjobs@apple.com", from: "#user_ids", multiple: true)
- select "Reporter", from: "access_level"
- end
-
- click_button "Add users to group"
- end
-
- step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
- page.within '.well-list' do
- expect(page).to have_content('sjobs@apple.com')
- expect(page).to have_content('invited')
- expect(page).to have_content('Reporter')
- end
- end
-
step 'I should see group "Owned" projects list' do
- Group.find_by(name: "Owned").projects.each do |project|
+ owned_group.projects.each do |project|
expect(page).to have_link project.name
end
end
@@ -94,36 +50,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
end
end
- step 'I select user "Mary Jane" from list with role "Reporter"' do
- user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane")
- click_button 'Add members'
- page.within ".users-group-form" do
- select2(user.id, from: "#user_ids", multiple: true)
- select "Reporter", from: "access_level"
- end
- click_button "Add users to group"
- end
-
- step 'I should see user "John Doe" in team list' do
- projects_with_access = find(".panel .well-list")
- expect(projects_with_access).to have_content("John Doe")
- end
-
- step 'I should not see user "John Doe" in team list' do
- projects_with_access = find(".panel .well-list")
- expect(projects_with_access).not_to have_content("John Doe")
- end
-
- step 'I should see user "Mary Jane" in team list' do
- projects_with_access = find(".panel .well-list")
- expect(projects_with_access).to have_content("Mary Jane")
- end
-
- step 'I should not see user "Mary Jane" in team list' do
- projects_with_access = find(".panel .well-list")
- expect(projects_with_access).not_to have_content("Mary Jane")
- end
-
step 'project from group "Owned" has issues assigned to me' do
create :issue,
project: project,
@@ -154,12 +80,12 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'I change group "Owned" avatar' do
attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
click_button "Save group"
- Group.find_by(name: "Owned").reload
+ owned_group.reload
end
step 'I should see new group "Owned" avatar' do
- expect(Group.find_by(name: "Owned").avatar).to be_instance_of AvatarUploader
- expect(Group.find_by(name: "Owned").avatar.url).to eq "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/banana_sample.gif"
+ expect(owned_group.avatar).to be_instance_of AvatarUploader
+ expect(owned_group.avatar.url).to eq "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/banana_sample.gif"
end
step 'I should see the "Remove avatar" button' do
@@ -169,83 +95,22 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'I have group "Owned" avatar' do
attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
click_button "Save group"
- Group.find_by(name: "Owned").reload
+ owned_group.reload
end
step 'I remove group "Owned" avatar' do
click_link "Remove avatar"
- Group.find_by(name: "Owned").reload
+ owned_group.reload
end
step 'I should not see group "Owned" avatar' do
- expect(Group.find_by(name: "Owned").avatar?).to eq false
+ expect(owned_group.avatar?).to eq false
end
step 'I should not see the "Remove avatar" button' do
expect(page).not_to have_link("Remove avatar")
end
- step 'I click on the "Remove User From Group" button for "John Doe"' do
- find(:css, 'li', text: "John Doe").find(:css, 'a.btn-remove').click
- # poltergeist always confirms popups.
- end
-
- step 'I click on the "Remove User From Group" button for "Mary Jane"' do
- find(:css, 'li', text: "Mary Jane").find(:css, 'a.btn-remove').click
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Remove User From Group" button for "John Doe"' do
- expect(find(:css, 'li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove')
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Remove User From Group" button for "Mary Jane"' do
- expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove')
- # poltergeist always confirms popups.
- end
-
- step 'I search for \'Mary\' member' do
- page.within '.member-search-form' do
- fill_in 'search', with: 'Mary'
- click_button 'Search'
- end
- end
-
- step 'I click on group milestones' do
- click_link 'Milestones'
- end
-
- step 'I should see group milestones index page has no milestones' do
- expect(page).to have_content('No milestones to show')
- end
-
- step 'Group has projects with milestones' do
- group_milestone
- end
-
- step 'I should see group milestones index page with milestones' do
- expect(page).to have_content('Version 7.2')
- expect(page).to have_content('GL-113')
- expect(page).to have_link('2 Issues', href: issues_group_path("owned", milestone_title: "Version 7.2"))
- expect(page).to have_link('3 Merge Requests', href: merge_requests_group_path("owned", milestone_title: "GL-113"))
- end
-
- step 'I click on one group milestone' do
- click_link 'GL-113'
- end
-
- step 'I should see group milestone with descriptions and expiry date' do
- expect(page).to have_content('expires at Aug 20, 2114')
- end
-
- step 'I should see group milestone with all issues and MRs assigned to that milestone' do
- expect(page).to have_content('Milestone GL-113')
- expect(page).to have_content('Progress: 0 closed – 4 open')
- expect(page).to have_link(@issue1.title, href: namespace_project_issue_path(@project1.namespace, @project1, @issue1))
- expect(page).to have_link(@mr3.title, href: namespace_project_merge_request_path(@project3.namespace, @project3, @mr3))
- end
-
step 'Group "Owned" has archived project' do
group = Group.find_by(name: 'Owned')
create(:project, namespace: group, archived: true, path: "archived-project")
@@ -255,79 +120,13 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
end
- protected
+ private
def assigned_to_me(key)
project.send(key).where(assignee_id: current_user.id)
end
def project
- Group.find_by(name: "Owned").projects.first
- end
-
- def group_milestone
- group = Group.find_by(name: "Owned")
-
- @project1 = create :project,
- group: group
- project2 = create :project,
- path: 'gitlab-ci',
- group: group
- @project3 = create :project,
- path: 'cookbook-gitlab',
- group: group
- milestone1_project1 = create :milestone,
- title: "Version 7.2",
- project: @project1
- milestone1_project2 = create :milestone,
- title: "Version 7.2",
- project: project2
- milestone1_project3 = create :milestone,
- title: "Version 7.2",
- project: @project3
- milestone2_project1 = create :milestone,
- title: "GL-113",
- project: @project1
- milestone2_project2 = create :milestone,
- title: "GL-113",
- project: project2
- milestone2_project3 = create :milestone,
- title: "GL-113",
- project: @project3,
- due_date: '2114-08-20',
- description: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry'
- @issue1 = create :issue,
- project: @project1,
- assignee: current_user,
- author: current_user,
- milestone: milestone2_project1
- issue2 = create :issue,
- project: project2,
- assignee: current_user,
- author: current_user,
- milestone: milestone1_project2
- issue3 = create :issue,
- project: @project3,
- assignee: current_user,
- author: current_user,
- milestone: milestone1_project1
- mr1 = create :merge_request,
- source_project: @project1,
- target_project: @project1,
- assignee: current_user,
- author: current_user,
- milestone: milestone2_project1
- mr2 = create :merge_request,
- source_project: project2,
- target_project: project2,
- assignee: current_user,
- author: current_user,
- milestone: milestone2_project2
- @mr3 = create :merge_request,
- source_project: @project3,
- target_project: @project3,
- assignee: current_user,
- author: current_user,
- milestone: milestone2_project3
+ owned_group.projects.first
end
end
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index 8cf24705a5e..40b2aa7c357 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -59,7 +59,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step 'I should not see the "Remove avatar" button' do
expect(page).not_to have_link("Remove avatar")
end
-
+
step 'I should see the gravatar host link' do
expect(page).to have_link("gravatar.com")
end
@@ -159,10 +159,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I should see my user page' do
- expect(page).to have_content "User Activity"
-
- page.within '.navbar-gitlab' do
+ page.within ".cover-block" do
expect(page).to have_content current_user.name
+ expect(page).to have_content current_user.username
end
end
@@ -176,7 +175,13 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I should see groups I belong to' do
- expect(page).to have_css('.profile-groups-avatars', visible: true)
+ page.within ".content" do
+ click_link "Groups"
+ end
+
+ page.within "#groups" do
+ expect(page).to have_content @group.name
+ end
end
step 'I click on new application button' do
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 56f1f06fb06..e5b3f27135d 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -103,11 +103,21 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'commit has ci status' do
- @project.enable_ci(@user)
- create :ci_commit, project: @project.gitlab_ci_project, sha: sample_commit.id
+ @project.enable_ci
+ ci_commit = create :ci_commit, gl_project: @project, sha: sample_commit.id
+ create :ci_build, commit: ci_commit
end
step 'I see commit ci info' do
- expect(page).to have_content "build: skipped"
+ expect(page).to have_content "build: pending"
+ end
+
+ step 'I click status link' do
+ find('.commit-ci-menu').click_link "Builds"
+ end
+
+ step 'I see builds list' do
+ expect(page).to have_content "build: pending"
+ expect(page).to have_content "Latest builds"
end
end
diff --git a/features/steps/project/commits/tags.rb b/features/steps/project/commits/tags.rb
index e6f8faf50fd..eff4234a44a 100644
--- a/features/steps/project/commits/tags.rb
+++ b/features/steps/project/commits/tags.rb
@@ -18,6 +18,18 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps
click_button 'Create tag'
end
+ step 'I submit new tag form with release notes' do
+ fill_in 'tag_name', with: 'v7.0'
+ fill_in 'ref', with: 'master'
+ fill_in 'release_description', with: 'Awesome release notes'
+ click_button 'Create tag'
+ end
+
+ step 'I fill release notes and submit form' do
+ fill_in 'release_description', with: 'Awesome release notes'
+ click_button 'Save changes'
+ end
+
step 'I submit new tag form with invalid name' do
fill_in 'tag_name', with: 'v 1.0'
fill_in 'ref', with: 'master'
@@ -52,31 +64,27 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps
expect(page).to have_content 'Tag already exists'
end
+ step "I visit tag 'v1.1.0' page" do
+ click_link 'v1.1.0'
+ end
+
step "I delete tag 'v1.1.0'" do
- page.within '.tags' do
+ page.within('.content') do
first('.btn-remove').click
- sleep 0.05
end
end
step "I should not see tag 'v1.1.0'" do
page.within '.tags' do
- expect(page.all(visible: true)).not_to have_content 'v1.1.0'
+ expect(page).not_to have_link 'v1.1.0'
end
end
- step 'I delete all tags' do
- page.within '.tags' do
- page.all('.btn-remove').each do |remove|
- remove.click
- sleep 0.05
- end
- end
+ step 'I click edit tag link' do
+ click_link 'Edit release notes'
end
- step 'I should see tags info message' do
- page.within '.tags' do
- expect(page).to have_content 'Repository has no tags yet.'
- end
+ step 'I should see tag release notes' do
+ expect(page).to have_content 'Awesome release notes'
end
end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 370960845cc..b0230add34f 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -5,8 +5,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'I click link "Fork"' do
expect(page).to have_content "Shop"
- expect(page).to have_content "Fork"
- click_link "Fork"
+ click_link "Fork project"
end
step 'I am a member of project "Shop"' do
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 2a333222fb2..d3675060994 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -39,14 +39,14 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
select "fix", from: "merge_request_source_branch"
select "master", from: "merge_request_target_branch"
- click_button "Compare branches"
+ click_button "Compare branches and continue"
- expect(page).to have_content "New merge request"
+ expect(page).to have_content "New Merge Request"
fill_in "merge_request_title", with: "Merge Request On Forked Project"
end
step 'I submit the merge request' do
- click_button "Submit new merge request"
+ click_button "Submit merge request"
end
step 'I follow the target commit link' do
@@ -112,11 +112,10 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
end
step 'I fill out an invalid "Merge Request On Forked Project" merge request' do
- select "Select branch", from: "merge_request_target_branch"
expect(find(:select, "merge_request_source_project_id", {}).value).to eq @forked_project.id.to_s
expect(find(:select, "merge_request_target_project_id", {}).value).to eq @project.id.to_s
expect(find(:select, "merge_request_source_branch", {}).value).to eq ""
- expect(find(:select, "merge_request_target_branch", {}).value).to eq ""
+ expect(find(:select, "merge_request_target_branch", {}).value).to eq "master"
click_button "Compare branches"
end
diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb
index 9453d636445..98f31f3b76a 100644
--- a/features/steps/project/graph.rb
+++ b/features/steps/project/graph.rb
@@ -25,13 +25,13 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
step 'page should have CI graphs' do
expect(page).to have_content 'Overall'
- expect(page).to have_content 'Builds chart for last week'
- expect(page).to have_content 'Builds chart for last month'
- expect(page).to have_content 'Builds chart for last year'
+ expect(page).to have_content 'Builds for last week'
+ expect(page).to have_content 'Builds for last month'
+ expect(page).to have_content 'Builds for last year'
expect(page).to have_content 'Commit duration in minutes for last 30 commits'
end
def project
- project ||= Project.find_by(name: "Shop")
+ @project ||= Project.find_by(name: "Shop")
end
end
diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb
index df4a23a3716..be4db770948 100644
--- a/features/steps/project/hooks.rb
+++ b/features/steps/project/hooks.rb
@@ -70,8 +70,6 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
step 'I should see hook service down error message' do
expect(page).to have_selector '.flash-alert',
- text: 'Hook execution failed. '\
- 'Ensure hook URL is correct and '\
- 'service is up.'
+ text: 'Hook execution failed: Exception from'
end
end
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
new file mode 100644
index 00000000000..325eaf2ea6a
--- /dev/null
+++ b/features/steps/project/issues/award_emoji.rb
@@ -0,0 +1,48 @@
+class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedPaths
+ include Select2Helper
+
+ step 'I visit "Bugfix" issue page' do
+ visit namespace_project_issue_path(@project.namespace, @project, @issue)
+ end
+
+ step 'I click to emoji-picker' do
+ page.within '.awards-controls' do
+ page.find('.add-award').click
+ end
+ end
+
+ step 'I click to emoji in the picker' do
+ page.within '.awards-menu' do
+ page.first('img').click
+ end
+ end
+
+ step 'I can remove it by clicking to icon' do
+ page.within '.awards' do
+ page.first('.award').click
+ expect(page).to_not have_selector '.award'
+ end
+ end
+
+ step 'I have award added' do
+ page.within '.awards' do
+ expect(page).to have_selector '.award'
+ expect(page.find('.award .counter')).to have_content '1'
+ end
+ end
+
+ step 'project "Shop" has issue "Bugfix"' do
+ @project = Project.find_by(name: 'Shop')
+ @issue = create(:issue, title: 'Bugfix', project: project)
+ end
+
+ step 'I leave comment with a single emoji' do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: ':smile:'
+ click_button 'Add Comment'
+ end
+ end
+end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 239392eab96..a13044c3ae1 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -65,20 +65,20 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
step 'I see current user as the first user' do
expect(page).to have_selector('.user-result', visible: true, count: 4)
users = page.all('.user-name')
- expect(users[0].text).to eq 'Any'
+ expect(users[0].text).to eq 'Any Assignee'
expect(users[1].text).to eq 'Unassigned'
expect(users[2].text).to eq current_user.name
end
step 'I submit new issue "500 error on profile"' do
fill_in "issue_title", with: "500 error on profile"
- click_button "Submit new issue"
+ click_button "Submit issue"
end
step 'I submit new issue "500 error on profile" with label \'bug\'' do
fill_in "issue_title", with: "500 error on profile"
select 'bug', from: "Labels"
- click_button "Submit new issue"
+ click_button "Submit issue"
end
step 'I click link "500 error on profile"' do
@@ -86,7 +86,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I should see label \'bug\' with issue' do
- page.within '.issue-show-labels' do
+ page.within '.issuable-show-labels' do
expect(page).to have_content 'bug'
end
end
@@ -223,11 +223,11 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'project \'Shop\' has issue \'Bugfix1\' with description: \'Description for issue1\'' do
- issue = create(:issue, title: 'Bugfix1', description: 'Description for issue1', project: project)
+ create(:issue, title: 'Bugfix1', description: 'Description for issue1', project: project)
end
step 'project \'Shop\' has issue \'Feature1\' with description: \'Feature submitted for issue1\'' do
- issue = create(:issue, title: 'Feature1', description: 'Feature submitted for issue1', project: project)
+ create(:issue, title: 'Feature1', description: 'Feature submitted for issue1', project: project)
end
step 'I fill in issue search with \'Description for issue1\'' do
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
index d656acf4220..2ab8956867b 100644
--- a/features/steps/project/issues/labels.rb
+++ b/features/steps/project/issues/labels.rb
@@ -9,7 +9,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I remove label \'bug\'' do
page.within "#label_#{bug_label.id}" do
- click_link 'Remove'
+ click_link 'Delete'
end
end
@@ -31,20 +31,20 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I submit new label \'support\'' do
fill_in 'Title', with: 'support'
- fill_in 'Background Color', with: '#F95610'
- click_button 'Save'
+ fill_in 'Background color', with: '#F95610'
+ click_button 'Create Label'
end
step 'I submit new label \'bug\'' do
fill_in 'Title', with: 'bug'
- fill_in 'Background Color', with: '#F95610'
- click_button 'Save'
+ fill_in 'Background color', with: '#F95610'
+ click_button 'Create Label'
end
step 'I submit new label with invalid color' do
fill_in 'Title', with: 'support'
- fill_in 'Background Color', with: '#12'
- click_button 'Save'
+ fill_in 'Background color', with: '#12'
+ click_button 'Create Label'
end
step 'I should see label label exist error message' do
@@ -55,7 +55,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I should see label color error message' do
page.within '.label-form' do
- expect(page).to have_content 'Color is invalid'
+ expect(page).to have_content 'Color must be a valid color code'
end
end
@@ -85,8 +85,8 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I change label \'bug\' to \'fix\'' do
fill_in 'Title', with: 'fix'
- fill_in 'Background Color', with: '#F15610'
- click_button 'Save'
+ fill_in 'Background color', with: '#F15610'
+ click_button 'Save changes'
end
step 'I should see label \'fix\'' do
diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb
index c8708572ec6..e2eda511497 100644
--- a/features/steps/project/issues/milestones.rb
+++ b/features/steps/project/issues/milestones.rb
@@ -63,7 +63,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
end
step 'I click link to remove milestone' do
- click_link 'Remove'
+ click_link 'Delete'
end
step 'I should see no milestones' do
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index c92998631ff..822cf0ffe1c 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -40,6 +40,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(page).to have_content "Bug NS-04"
end
+ step 'I should not see "master" branch' do
+ expect(page).not_to have_content "master"
+ end
+
+ step 'I should see "other_branch" branch' do
+ expect(page).to have_content "other_branch"
+ end
+
step 'I should see "Bug NS-04" in merge requests' do
expect(page).to have_content "Bug NS-04"
end
@@ -78,7 +86,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
select "feature", from: "merge_request_target_branch"
click_button "Compare branches"
fill_in "merge_request_title", with: "Wiki Feature"
- click_button "Submit new merge request"
+ click_button "Submit merge request"
end
step 'project "Shop" have "Bug NS-04" open merge request' do
@@ -93,6 +101,18 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
)
end
+ step 'project "Shop" have "Bug NS-06" open merge request' do
+ create(:merge_request,
+ title: "Bug NS-06",
+ source_project: project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'other_branch',
+ author: project.users.first,
+ description: "# Description header"
+ )
+ end
+
step 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do
create(:merge_request_with_diffs,
title: "Bug NS-05",
@@ -224,43 +244,43 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
end
- step 'I click link "Hide inline discussion" of the second file' do
- page.within '.files [id^=diff]:nth-child(2)' do
+ step 'I click link "Hide inline discussion" of the third file' do
+ page.within '.files [id^=diff]:nth-child(3)' do
find('.js-toggle-diff-comments').trigger('click')
end
end
- step 'I click link "Show inline discussion" of the second file' do
- page.within '.files [id^=diff]:nth-child(2)' do
+ step 'I click link "Show inline discussion" of the third file' do
+ page.within '.files [id^=diff]:nth-child(3)' do
find('.js-toggle-diff-comments').trigger('click')
end
end
- step 'I should not see a comment like "Line is wrong" in the second file' do
- page.within '.files [id^=diff]:nth-child(2)' do
+ step 'I should not see a comment like "Line is wrong" in the third file' do
+ page.within '.files [id^=diff]:nth-child(3)' do
expect(page).not_to have_visible_content "Line is wrong"
end
end
- step 'I should see a comment like "Line is wrong" in the second file' do
- page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do
+ step 'I should see a comment like "Line is wrong" in the third file' do
+ page.within '.files [id^=diff]:nth-child(3) .note-body > .note-text' do
expect(page).to have_visible_content "Line is wrong"
end
end
- step 'I should not see a comment like "Line is wrong here" in the second file' do
- page.within '.files [id^=diff]:nth-child(2)' do
+ step 'I should not see a comment like "Line is wrong here" in the third file' do
+ page.within '.files [id^=diff]:nth-child(3)' do
expect(page).not_to have_visible_content "Line is wrong here"
end
end
- step 'I should see a comment like "Line is wrong here" in the second file' do
- page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do
+ step 'I should see a comment like "Line is wrong here" in the third file' do
+ page.within '.files [id^=diff]:nth-child(3) .note-body > .note-text' do
expect(page).to have_visible_content "Line is wrong here"
end
end
- step 'I leave a comment like "Line is correct" on line 12 of the first file' do
+ step 'I leave a comment like "Line is correct" on line 12 of the second file' do
init_diff_note_first_file
page.within(".js-discussion-note-form") do
@@ -268,12 +288,12 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
click_button "Add Comment"
end
- page.within ".files [id^=diff]:nth-child(1) .note-body > .note-text" do
+ page.within ".files [id^=diff]:nth-child(2) .note-body > .note-text" do
expect(page).to have_content "Line is correct"
end
end
- step 'I leave a comment like "Line is wrong" on line 39 of the second file' do
+ step 'I leave a comment like "Line is wrong" on line 39 of the third file' do
init_diff_note_second_file
page.within(".js-discussion-note-form") do
@@ -282,8 +302,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
end
- step 'I should still see a comment like "Line is correct" in the first file' do
- page.within '.files [id^=diff]:nth-child(1) .note-body > .note-text' do
+ step 'I should still see a comment like "Line is correct" in the second file' do
+ page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do
expect(page).to have_visible_content "Line is correct"
end
end
@@ -303,7 +323,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should see comments on the side-by-side diff page' do
- page.within '.files [id^=diff]:nth-child(1) .parallel .note-body > .note-text' do
+ page.within '.files [id^=diff]:nth-child(2) .parallel .note-body > .note-text' do
expect(page).to have_visible_content "Line is correct"
end
end
@@ -338,6 +358,19 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(page).to have_content('diff --git')
end
+ step '"Bug NS-05" has CI status' do
+ project = merge_request.source_project
+ project.enable_ci
+ ci_commit = create :ci_commit, gl_project: project, sha: merge_request.last_commit.id
+ create :ci_build, commit: ci_commit
+ end
+
+ step 'I should see merge request "Bug NS-05" with CI status' do
+ page.within ".mr-list" do
+ expect(page).to have_link "Build status: pending"
+ end
+ end
+
def merge_request
@merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
end
diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb
new file mode 100644
index 00000000000..383c055c4ef
--- /dev/null
+++ b/features/steps/project/merge_requests/acceptance.rb
@@ -0,0 +1,39 @@
+class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
+ include LoginHelpers
+ include GitlabRoutingHelper
+
+ step 'I am on the Merge Request detail page' do
+ visit merge_request_path(@merge_request)
+ end
+
+ step 'I click on "Remove source branch" option' do
+ check('Remove source branch')
+ end
+
+ step 'I click on Accept Merge Request' do
+ click_button('Accept Merge Request')
+ end
+
+ step 'I should see the Remove Source Branch button' do
+ expect(page).to have_link('Remove Source Branch')
+ end
+
+ step 'I should not see the Remove Source Branch button' do
+ expect(page).not_to have_link('Remove Source Branch')
+ end
+
+ step 'There is an open Merge Request' do
+ @user = create(:user)
+ @project = create(:project, :public)
+ @project_member = create(:project_member, user: @user, project: @project, access_level: ProjectMember::DEVELOPER)
+ @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
+ end
+
+ step 'I am signed in as a developer of the project' do
+ login_as(@user)
+ end
+
+ step 'I should see merge request merged' do
+ expect(page).to have_content('The changes were merged into')
+ end
+end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 15f77734cb2..9ca7c8ebbc7 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -86,13 +86,13 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should see project "Forum" README' do
- page.within('#README') do
+ page.within('.readme-holder') do
expect(page).to have_content 'Sample repo for testing gitlab features'
end
end
step 'I should see project "Shop" README' do
- page.within('#README') do
+ page.within('.readme-holder') do
expect(page).to have_content 'testme'
end
end
@@ -124,11 +124,11 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should see back to dashboard button' do
- expect(page).to have_content 'Back to dashboard'
+ expect(page).to have_content 'Go to dashboard'
end
step 'I should see back to group button' do
- expect(page).to have_content 'Back to group'
+ expect(page).to have_content 'Go to group'
end
step 'I click notifications drop down button' do
diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb
index 0e724138a8a..1ffd5cb9de5 100644
--- a/features/steps/project/redirects.rb
+++ b/features/steps/project/redirects.rb
@@ -39,7 +39,6 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps
step 'Authenticate' do
admin = create(:admin)
- project = Project.find_by(name: 'Community')
fill_in "user_login", with: admin.email
fill_in "user_password", with: admin.password
click_button "Sign in"
@@ -54,7 +53,6 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps
step 'I get redirected to signin page where I sign in' do
admin = create(:admin)
- project = Project.find_by(name: 'Enterprise')
fill_in "user_login", with: admin.email
fill_in "user_password", with: admin.password
click_button "Sign in"
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index d3b462bfd31..1c700df0c63 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -202,6 +202,10 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
expect(find_field('Username').value).to eq 'user'
end
+ step 'I should see empty field Change Password' do
+ expect(find_field('Change Password').value).to be_nil
+ end
+
step 'I click JetBrains TeamCity CI service link' do
click_link 'JetBrains TeamCity CI'
end
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index db8ad08bb9e..a3aef9bf8c3 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -22,7 +22,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
end
step 'I click link "New Snippet"' do
- click_link "Add new snippet"
+ click_link "New Snippet"
end
step 'I click link "Snippet one"' do
@@ -42,13 +42,13 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
end
step 'I click link "Edit"' do
- page.within ".file-title" do
+ page.within ".page-title" do
click_link "Edit"
end
end
- step 'I click link "Remove Snippet"' do
- click_link "remove"
+ step 'I click link "Delete"' do
+ click_link "Delete"
end
step 'I submit new snippet "Snippet three"' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index a1a49dd58a6..05d1346d006 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -71,31 +71,39 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I fill the new branch name' do
- fill_in :new_branch, with: 'new_branch_name'
+ fill_in :new_branch, with: 'new_branch_name', visible: true
end
step 'I fill the new file name with an illegal name' do
fill_in :file_name, with: 'Spaces Not Allowed'
end
+ step 'I fill the new file name with a new directory' do
+ fill_in :file_name, with: new_file_name_with_directory
+ end
+
step 'I fill the commit message' do
fill_in :commit_message, with: 'Not yet a commit message.', visible: true
end
step 'I click link "Diff"' do
- click_link 'Preview changes'
+ click_link 'Preview Changes'
end
step 'I click on "Commit Changes"' do
click_button 'Commit Changes'
end
- step 'I click on "Remove"' do
- click_button 'Remove'
+ step 'I click on "Create directory"' do
+ click_button 'Create directory'
end
- step 'I click on "Remove file"' do
- click_button 'Remove file'
+ step 'I click on "Delete"' do
+ click_button 'Delete'
+ end
+
+ step 'I click on "Delete file"' do
+ click_button 'Delete file'
end
step 'I click on "Replace"' do
@@ -110,21 +118,32 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(page).to have_css '.line_holder.new'
end
- step 'I click on "new file" link in repo' do
- click_link 'new-file-link'
+ step 'I click on "New file" link in repo' do
+ find('.add-to-tree').click
+ click_link 'Create file'
end
- step 'I can see new file page' do
- expect(page).to have_content "new file"
- expect(page).to have_content "Commit message"
+ step 'I click on "Upload file" link in repo' do
+ find('.add-to-tree').click
+ click_link 'Upload file'
+ end
+
+ step 'I click on "New directory" link in repo' do
+ find('.add-to-tree').click
+ click_link 'New directory'
end
- step 'I can see "upload an existing one"' do
- expect(page).to have_content "upload an existing one"
+ step 'I fill the new directory name' do
+ fill_in :dir_name, with: new_dir_name
end
- step 'I click on "upload"' do
- click_link 'upload'
+ step 'I fill an existing directory name' do
+ fill_in :dir_name, with: 'files'
+ end
+
+ step 'I can see new file page' do
+ expect(page).to have_content "New File"
+ expect(page).to have_content "Commit message"
end
step 'I click on "Upload file"' do
@@ -173,7 +192,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I see Browse dir link' do
- expect(page).to have_link 'Browse Dir »'
+ expect(page).to have_link 'Browse Directory »'
expect(page).not_to have_link 'Browse Code »'
end
@@ -185,13 +204,13 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I see Browse file link' do
expect(page).to have_link 'Browse File »'
- expect(page).not_to have_link 'Browse Code »'
+ expect(page).not_to have_link 'Browse Files »'
end
step 'I see Browse code link' do
- expect(page).to have_link 'Browse Code »'
+ expect(page).to have_link 'Browse Files »'
expect(page).not_to have_link 'Browse File »'
- expect(page).not_to have_link 'Browse Dir »'
+ expect(page).not_to have_link 'Browse Directory »'
end
step 'I click on Permalink' do
@@ -206,10 +225,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'master/.gitignore'))
end
- step 'I am redirected to the ".gitignore" on new branch' do
- expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'new_branch_name/.gitignore'))
- end
-
step 'I am redirected to the permalink URL' do
expect(current_path).to(
eq(namespace_project_blob_path(@project.namespace, @project,
@@ -223,15 +238,28 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
@project.namespace, @project, 'master/' + new_file_name))
end
- step 'I am redirected to the new file on new branch' do
+ step 'I am redirected to the new file with directory' do
expect(current_path).to eq(namespace_project_blob_path(
- @project.namespace, @project, 'new_branch_name/' + new_file_name))
+ @project.namespace, @project, 'master/' + new_file_name_with_directory))
+ end
+
+ step 'I am redirected to the new merge request page' do
+ expect(current_path).to eq(new_namespace_project_merge_request_path(@project.namespace, @project))
+ end
+
+ step 'I am redirected to the root directory' do
+ expect(current_path).to eq(namespace_project_tree_path(
+ @project.namespace, @project, 'master/'))
end
step "I don't see the permalink link" do
expect(page).not_to have_link('permalink')
end
+ step 'I see "Unable to create directory"' do
+ expect(page).to have_content('Directory already exists')
+ end
+
step 'I see a commit error message' do
expect(page).to have_content('Your changes could not be committed')
end
@@ -251,6 +279,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
select "'test'", from: 'ref'
end
+ step "I switch ref to fix" do
+ select "fix", from: 'ref'
+ end
+
step "I see the ref 'test' has been selected" do
expect(page).to have_selector '.select2-chosen', text: "'test'"
end
@@ -259,11 +291,20 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
visit namespace_project_tree_path(@project.namespace, @project, "'test'")
end
+ step "I visit the fix tree" do
+ visit namespace_project_tree_path(@project.namespace, @project, "fix/.testdir")
+ end
+
step 'I see the commit data' do
expect(page).to have_css('.tree-commit-link', visible: true)
expect(page).not_to have_content('Loading commit data...')
end
+ step 'I see the commit data for a directory with a leading dot' do
+ expect(page).to have_css('.tree-commit-link', visible: true)
+ expect(page).not_to have_content('Loading commit data...')
+ end
+
private
def set_new_content
@@ -287,6 +328,18 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
'not_a_file.md'
end
+ # Constant value that is a valid filename with directory and
+ # not a filename present at root of the seed repository.
+ def new_file_name_with_directory
+ 'foo/bar/baz.txt'
+ end
+
+ # Constant value that is a valid directory and
+ # not a directory present at root of the seed repository.
+ def new_dir_name
+ 'new_dir/subdir'
+ end
+
def drop_in_dropzone(file_path)
# Generate a fake input selector
page.execute_script <<-JS
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index c78e86fa1a7..3a4f7a6e01c 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -238,7 +238,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see new wiki page named test' do
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "test")
- expect(page).to have_content "Editing"
+ expect(page).to have_content "Edit Page test"
end
When 'I go back to wiki page home' do
@@ -252,7 +252,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see Gitlab API document' do
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "api")
- expect(page).to have_content "Editing"
+ expect(page).to have_content "Edit Page api"
end
step 'I click on Rake tasks link' do
@@ -261,7 +261,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see Rake tasks directory' do
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "raketasks")
- expect(page).to have_content "Editing"
+ expect(page).to have_content "Edit Page raketasks"
end
step 'I go directory which contains README file' do
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index 97d63016458..caad52def79 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -15,10 +15,6 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
expect(page).to have_content(user.username)
end
- step 'I click link "Add members"' do
- find(:css, 'button.btn-new').click
- end
-
step 'I select "Mike" as "Reporter"' do
user = User.find_by(name: "Mike")
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 02207dbffa6..91d227fadbf 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -5,7 +5,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
include SharedPaths
step 'I click on the Cancel button' do
- page.within(:css, ".form-actions") do
+ page.within(:css, ".wiki-form .form-actions") do
click_on "Cancel"
end
end
@@ -24,7 +24,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
expect(page).to have_content "link test"
click_link "link test"
- expect(page).to have_content "Editing"
+ expect(page).to have_content "Edit Page"
end
step 'I have an existing Wiki page' do
@@ -68,7 +68,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'I click on the "Delete this page" button' do
- click_on "Delete this page"
+ click_on "Delete"
end
step 'The page should be deleted' do
@@ -120,13 +120,13 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
step 'I should see the new wiki page form' do
expect(current_path).to match('wikis/image.jpg')
expect(page).to have_content('New Wiki Page')
- expect(page).to have_content('Editing - image.jpg')
+ expect(page).to have_content('Edit Page image.jpg')
end
step 'I create a New page with paths' do
click_on 'New Page'
fill_in 'Page slug', with: 'one/two/three'
- click_on 'Build'
+ click_on 'Create Page'
fill_in "wiki_content", with: 'wiki content'
click_on "Create page"
expect(current_path).to include 'one/two/three'
@@ -135,7 +135,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
step 'I create a New page with an invalid name' do
click_on 'New Page'
fill_in 'Page slug', with: 'invalid name'
- click_on 'Build'
+ click_on 'Create Page'
end
step 'I should see an error message' do
@@ -156,7 +156,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'I should see the Editing page' do
- expect(page).to have_content('Editing')
+ expect(page).to have_content('Edit Page')
end
step 'I view the page history of a Wiki page that has a path' do
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index 72621911a37..dd466cde28d 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -87,6 +87,17 @@ module SharedDiffNote
end
end
+ step 'I write a diff comment like ":smile:"' do
+ page.within(diff_file_selector) do
+ click_diff_line(sample_commit.line_code)
+
+ page.within("form[rel$='#{sample_commit.line_code}']") do
+ fill_in 'note[note]', with: ':smile:'
+ click_button('Add Comment')
+ end
+ end
+ end
+
step 'I submit the diff comment' do
page.within(diff_file_selector) do
click_button("Add Comment")
@@ -197,6 +208,12 @@ module SharedDiffNote
end
end
+ step 'I should see a diff comment with an emoji image' do
+ page.within("#{diff_file_selector} .note") do
+ expect(page).to have_xpath("//img[@alt=':smile:']")
+ end
+ end
+
step 'I click side-by-side diff button' do
find('#parallel-diff-btn').trigger('click')
end
diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb
index 2d17fb34ccb..58581653f28 100644
--- a/features/steps/shared/group.rb
+++ b/features/steps/shared/group.rb
@@ -37,8 +37,12 @@ module SharedGroup
group = Group.find_by(name: groupname) || create(:group, name: groupname)
group.add_user(user, role)
project ||= create(:project, namespace: group, path: "project#{@project_count}")
- event ||= create(:closed_issue_event, project: project)
+ create(:closed_issue_event, project: project)
project.team << [user, :master]
@project_count += 1
end
+
+ def owned_group
+ @owned_group ||= Group.find_by(name: "Owned")
+ end
end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index eb978620da6..c74a5fd3bc7 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -31,6 +31,10 @@ module SharedPaths
visit merge_requests_group_path(Group.find_by(name: "Owned"))
end
+ step 'I visit group "Owned" milestones page' do
+ visit group_milestones_path(Group.find_by(name: "Owned"))
+ end
+
step 'I visit group "Owned" members page' do
visit group_group_members_path(Group.find_by(name: "Owned"))
end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index fa841f67510..7021fac5fe4 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -199,11 +199,18 @@ module SharedProject
step 'project "Shop" has CI enabled' do
project = Project.find_by(name: "Shop")
- project.enable_ci(@user)
+ project.enable_ci
end
step 'project "Shop" has CI build' do
project = Project.find_by(name: "Shop")
- create :ci_commit, project: project.gitlab_ci_project, sha: project.commit.sha
+ create :ci_commit, gl_project: project, sha: project.commit.sha
+ end
+
+ step 'I should see last commit with CI status' do
+ page.within ".project-last-commit" do
+ expect(page).to have_content(project.commit.sha[0..6])
+ expect(page).to have_content("skipped")
+ end
end
end
diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb
index c67e5e4a06a..33ff7084e30 100644
--- a/features/steps/shared/project_tab.rb
+++ b/features/steps/shared/project_tab.rb
@@ -46,7 +46,7 @@ module SharedProjectTab
step 'the active main tab should be Settings' do
page.within '.nav-sidebar' do
- expect(page).to have_content('Back to project')
+ expect(page).to have_content('Go to project')
end
end
diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb
index fc1e8d6e889..250cc5b94f3 100644
--- a/features/steps/shared/user.rb
+++ b/features/steps/shared/user.rb
@@ -9,6 +9,10 @@ module SharedUser
user_exists("Mary Jane", { username: "mary_jane" })
end
+ step 'gitlab user "Mike"' do
+ create(:user, name: "Mike")
+ end
+
protected
def user_exists(name, options = {})
diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb
index 6ff48e0c6b8..80d1ddeef05 100644
--- a/features/steps/snippets/snippets.rb
+++ b/features/steps/snippets/snippets.rb
@@ -13,13 +13,13 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps
end
step 'I click link "Edit"' do
- page.within ".file-title" do
+ page.within ".page-title" do
click_link "Edit"
end
end
- step 'I click link "Destroy"' do
- click_link "remove"
+ step 'I click link "Delete"' do
+ click_link "Delete"
end
step 'I submit new snippet "Personal snippet three"' do
diff --git a/features/steps/snippets/user.rb b/features/steps/snippets/user.rb
index dea3256229f..997c605bce2 100644
--- a/features/steps/snippets/user.rb
+++ b/features/steps/snippets/user.rb
@@ -32,19 +32,19 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps
end
step 'I click "Internal" filter' do
- page.within('.nav-tabs') do
+ page.within('.snippet-scope-menu') do
click_link "Internal"
end
end
step 'I click "Private" filter' do
- page.within('.nav-tabs') do
+ page.within('.snippet-scope-menu') do
click_link "Private"
end
end
step 'I click "Public" filter' do
- page.within('.nav-tabs') do
+ page.within('.snippet-scope-menu') do
click_link "Public"
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index c09488d3547..fe1bf8a4816 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -25,7 +25,7 @@ module API
format :json
content_type :txt, "text/plain"
- helpers APIHelpers
+ helpers Helpers
mount Groups
mount GroupMembers
@@ -46,10 +46,12 @@ module API
mount Services
mount Files
mount Commits
+ mount CommitStatus
mount Namespaces
mount Branches
mount Labels
mount Settings
mount Keys
+ mount Tags
end
end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
new file mode 100644
index 00000000000..2c0596c9dfb
--- /dev/null
+++ b/lib/api/commit_statuses.rb
@@ -0,0 +1,80 @@
+require 'mime/types'
+
+module API
+ # Project commit statuses API
+ class CommitStatus < Grape::API
+ resource :projects do
+ before { authenticate! }
+
+ # Get a commit's statuses
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # sha (required) - The commit hash
+ # ref (optional) - The ref
+ # stage (optional) - The stage
+ # name (optional) - The name
+ # all (optional) - Show all statuses, default: false
+ # Examples:
+ # GET /projects/:id/repository/commits/:sha/statuses
+ get ':id/repository/commits/:sha/statuses' do
+ authorize! :read_commit_statuses, user_project
+ sha = params[:sha]
+ ci_commit = user_project.ci_commit(sha)
+ not_found! 'Commit' unless ci_commit
+ statuses = ci_commit.statuses
+ statuses = statuses.latest unless parse_boolean(params[:all])
+ statuses = statuses.where(ref: params[:ref]) if params[:ref].present?
+ statuses = statuses.where(stage: params[:stage]) if params[:stage].present?
+ statuses = statuses.where(name: params[:name]) if params[:name].present?
+ present paginate(statuses), with: Entities::CommitStatus
+ end
+
+ # Post status to commit
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # sha (required) - The commit hash
+ # ref (optional) - The ref
+ # state (required) - The state of the status. Can be: pending, running, success, error or failure
+ # target_url (optional) - The target URL to associate with this status
+ # description (optional) - A short description of the status
+ # name or context (optional) - A string label to differentiate this status from the status of other systems. Default: "default"
+ # Examples:
+ # POST /projects/:id/statuses/:sha
+ post ':id/statuses/:sha' do
+ authorize! :create_commit_status, user_project
+ required_attributes! [:state]
+ attrs = attributes_for_keys [:ref, :target_url, :description, :context, :name]
+ commit = @project.commit(params[:sha])
+ not_found! 'Commit' unless commit
+
+ ci_commit = @project.ensure_ci_commit(commit.sha)
+
+ name = params[:name] || params[:context]
+ status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref])
+ status ||= GenericCommitStatus.new(commit: ci_commit, user: current_user)
+ status.update(attrs)
+
+ case params[:state].to_s
+ when 'running'
+ status.run
+ when 'success'
+ status.success
+ when 'failed'
+ status.drop
+ when 'canceled'
+ status.cancel
+ else
+ status.status = params[:state].to_s
+ end
+
+ if status.save
+ present status, with: Entities::CommitStatus
+ else
+ render_validation_error!(status)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 9620d36ac41..96b73df6af9 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -62,7 +62,7 @@ module API
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
expose :name, :name_with_namespace
expose :path, :path_with_namespace
- expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at
+ expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :created_at, :last_activity_at
expose :creator_id
expose :namespace
expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? }
@@ -95,25 +95,6 @@ module API
end
end
- class RepoTag < Grape::Entity
- expose :name
- expose :message do |repo_obj, _options|
- if repo_obj.respond_to?(:message)
- repo_obj.message
- else
- nil
- end
- end
-
- expose :commit do |repo_obj, options|
- if repo_obj.respond_to?(:commit)
- repo_obj.commit
- elsif options[:project]
- options[:project].repository.commit(repo_obj.target)
- end
- end
- end
-
class RepoObject < Grape::Entity
expose :name
@@ -149,6 +130,7 @@ module API
class RepoCommitDetail < RepoCommit
expose :parent_ids, :committed_date, :authored_date
+ expose :status
end
class ProjectSnippet < Grape::Entity
@@ -180,7 +162,9 @@ module API
end
class MergeRequest < ProjectEntity
- expose :target_branch, :source_branch, :upvotes, :downvotes
+ expose :target_branch, :source_branch
+ # deprecated, always returns 0
+ expose :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id
expose :label_names, as: :labels
@@ -210,6 +194,8 @@ module API
expose :author, using: Entities::UserBasic
expose :created_at
expose :system?, as: :system
+ expose :noteable_id, :noteable_type
+ # upvote? and downvote? are deprecated, always return false
expose :upvote?, as: :upvote
expose :downvote?, as: :downvote
end
@@ -228,11 +214,19 @@ module API
expose :created_at
end
+ class CommitStatus < Grape::Entity
+ expose :id, :sha, :ref, :status, :name, :target_url, :description,
+ :created_at, :started_at, :finished_at, :allow_failure
+ expose :author, using: Entities::UserBasic
+ end
+
class Event < Grape::Entity
expose :title, :project_id, :action_name
expose :target_id, :target_type, :author_id
expose :data, :target_title
expose :created_at
+ expose :note, using: Entities::Note, if: ->(event, options) { event.note? }
+ expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author }
expose :author_username do |event, options|
if event.author
@@ -255,6 +249,18 @@ module API
expose :notification_level
end
+ class ProjectService < Grape::Entity
+ expose :id, :title, :created_at, :updated_at, :active
+ expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events
+ # Expose serialized properties
+ expose :properties do |service, options|
+ field_names = service.fields.
+ select { |field| options[:include_passwords] || field[:type] != 'password' }.
+ map { |field| field[:name] }
+ service.properties.slice(*field_names)
+ end
+ end
+
class ProjectWithAccess < Project
expose :permissions do
expose :project_access, using: Entities::ProjectAccess do |project, options|
@@ -322,5 +328,35 @@ module API
expose :user_oauth_applications
expose :after_sign_out_path
end
+
+ class Release < Grape::Entity
+ expose :tag, as: :tag_name
+ expose :description
+ end
+
+ class RepoTag < Grape::Entity
+ expose :name
+ expose :message do |repo_obj, _options|
+ if repo_obj.respond_to?(:message)
+ repo_obj.message
+ else
+ nil
+ end
+ end
+
+ expose :commit do |repo_obj, options|
+ if repo_obj.respond_to?(:commit)
+ repo_obj.commit
+ elsif options[:project]
+ options[:project].repository.commit(repo_obj.target)
+ end
+ end
+
+ expose :release, using: Entities::Release do |repo_obj, options|
+ if options[:project]
+ options[:project].releases.find_by(tag: repo_obj.name)
+ end
+ end
+ end
end
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 308c84dd135..a7a768f8895 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -43,7 +43,8 @@ module API
# "content": "IyA9PSBTY2hlbWEgSW5mb3...",
# "ref": "master",
# "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83",
- # "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50"
+ # "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50",
+ # "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
# }
#
get ":id/repository/files" do
@@ -71,6 +72,7 @@ module API
ref: ref,
blob_id: blob.id,
commit_id: commit.id,
+ last_commit_id: user_project.repository.last_commit_for_path(commit.sha, file_path).id
}
else
not_found! 'File'
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 7fada98fcdc..92540ccf2b1 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -1,5 +1,5 @@
module API
- module APIHelpers
+ module Helpers
PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
PRIVATE_TOKEN_PARAM = :private_token
SUDO_HEADER ="HTTP_SUDO"
@@ -63,11 +63,11 @@ module API
user_project.build_missing_services
service_method = "#{underscored_service}_service"
-
+
send_service(service_method)
end
end
-
+
@project_service || not_found!("Service")
end
@@ -133,6 +133,12 @@ module API
authorize! :admin_project, user_project
end
+ def require_gitlab_workhorse!
+ unless env['HTTP_GITLAB_WORKHORSE'].present?
+ forbidden!('Request should be executed via GitLab Workhorse')
+ end
+ end
+
def can?(object, action, subject)
abilities.allowed?(object, action, subject)
end
@@ -149,7 +155,6 @@ module API
end
def attributes_for_keys(keys, custom_params = nil)
- params_hash = custom_params || params
attrs = {}
keys.each do |key|
if params[key].present? or (params.has_key?(key) and params[key] == false)
@@ -235,6 +240,10 @@ module API
render_api_error!(message || '409 Conflict', 409)
end
+ def file_to_large!
+ render_api_error!('413 Request Entity Too Large', 413)
+ end
+
def render_validation_error!(model)
if model.errors.any?
render_api_error!(model.errors.messages || '400 Bad Request', 400)
@@ -283,6 +292,44 @@ module API
end
end
+ # file helpers
+
+ def uploaded_file!(field, uploads_path)
+ if params[field]
+ bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename)
+ return params[field]
+ end
+
+ # sanitize file paths
+ # this requires all paths to exist
+ required_attributes! %W(#{field}.path)
+ uploads_path = File.realpath(uploads_path)
+ file_path = File.realpath(params["#{field}.path"])
+ bad_request!('Bad file path') unless file_path.start_with?(uploads_path)
+
+ UploadedFile.new(
+ file_path,
+ params["#{field}.name"],
+ params["#{field}.type"] || 'application/octet-stream',
+ )
+ end
+
+ def present_file!(path, filename, content_type = 'application/octet-stream')
+ filename ||= File.basename(path)
+ header['Content-Disposition'] = "attachment; filename=#{filename}"
+ header['Content-Transfer-Encoding'] = 'binary'
+ content_type content_type
+
+ # Support download acceleration
+ case headers['X-Sendfile-Type']
+ when 'X-Sendfile'
+ header['X-Sendfile'] = path
+ body
+ else
+ file FileStreamer.new(path)
+ end
+ end
+
private
def add_pagination_headers(paginated, per_page)
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 63ea2f05438..e7c5f808aea 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -76,6 +76,22 @@ module API
present merge_request, with: Entities::MergeRequest
end
+ # Show MR commits
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - The ID of MR
+ #
+ # Example:
+ # GET /projects/:id/merge_request/:merge_request_id/commits
+ #
+ get ':id/merge_request/:merge_request_id/commits' do
+ merge_request = user_project.merge_requests.
+ find(params[:merge_request_id])
+ authorize! :read_merge_request, merge_request
+ present merge_request.commits, with: Entities::RepoCommit
+ end
+
# Show MR changes
#
# Parameters:
@@ -99,7 +115,7 @@ module API
# id (required) - The ID of a project - this will be the source of the merge request
# source_branch (required) - The source branch
# target_branch (required) - The target branch
- # target_project - The target project of the merge request defaults to the :id of the project
+ # target_project_id - The target project of the merge request defaults to the :id of the project
# assignee_id - Assignee user ID
# title (required) - Title of MR
# description - Description of MR
@@ -249,8 +265,16 @@ module API
required_attributes! [:note]
merge_request = user_project.merge_requests.find(params[:merge_request_id])
- note = merge_request.notes.new(note: params[:note], project_id: user_project.id)
- note.author = current_user
+
+ authorize! :create_note, merge_request
+
+ opts = {
+ note: params[:note],
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id
+ }
+
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
if note.save
present note, with: Entities::MRNote
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index c2fb36b4143..6928fe0eb9d 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -7,8 +7,12 @@ module API
helpers do
def map_public_to_visibility_level(attrs)
publik = attrs.delete(:public)
- publik = parse_boolean(publik)
- attrs[:visibility_level] = Gitlab::VisibilityLevel::PUBLIC if !attrs[:visibility_level].present? && publik == true
+ if publik.present? && !attrs[:visibility_level].present?
+ publik = parse_boolean(publik)
+ # Since setting the public attribute to private could mean either
+ # private or internal, use the more conservative option, private.
+ attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE
+ end
attrs
end
end
@@ -75,6 +79,7 @@ module API
# description (optional) - short project description
# issues_enabled (optional)
# merge_requests_enabled (optional)
+ # builds_enabled (optional)
# wiki_enabled (optional)
# snippets_enabled (optional)
# namespace_id (optional) - defaults to user namespace
@@ -90,6 +95,7 @@ module API
:description,
:issues_enabled,
:merge_requests_enabled,
+ :builds_enabled,
:wiki_enabled,
:snippets_enabled,
:namespace_id,
@@ -117,6 +123,7 @@ module API
# default_branch (optional) - 'master' by default
# issues_enabled (optional)
# merge_requests_enabled (optional)
+ # builds_enabled (optional)
# wiki_enabled (optional)
# snippets_enabled (optional)
# public (optional) - if true same as setting visibility_level = 20
@@ -132,6 +139,7 @@ module API
:default_branch,
:issues_enabled,
:merge_requests_enabled,
+ :builds_enabled,
:wiki_enabled,
:snippets_enabled,
:public,
@@ -172,6 +180,7 @@ module API
# description (optional) - short project description
# issues_enabled (optional)
# merge_requests_enabled (optional)
+ # builds_enabled (optional)
# wiki_enabled (optional)
# snippets_enabled (optional)
# public (optional) - if true same as setting visibility_level = 20
@@ -185,6 +194,7 @@ module API
:default_branch,
:issues_enabled,
:merge_requests_enabled,
+ :builds_enabled,
:wiki_enabled,
:snippets_enabled,
:public,
@@ -246,8 +256,8 @@ module API
# Example Request:
# DELETE /projects/:id/fork
delete ":id/fork" do
- authenticated_as_admin!
- unless user_project.forked_project_link.nil?
+ authorize! :remove_fork_project, user_project
+ if user_project.forked?
user_project.forked_project_link.destroy
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 2d96c9666d2..d7c48639eba 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -16,41 +16,6 @@ module API
end
end
- # Get a project repository tags
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id/repository/tags
- get ":id/repository/tags" do
- present user_project.repo.tags.sort_by(&:name).reverse,
- with: Entities::RepoTag, project: user_project
- end
-
- # Create tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # ref (required) - Create tag from commit sha or branch
- # message (optional) - Specifying a message creates an annotated tag.
- # Example Request:
- # POST /projects/:id/repository/tags
- post ':id/repository/tags' do
- authorize_push_project
- message = params[:message] || nil
- result = CreateTagService.new(user_project, current_user).
- execute(params[:tag_name], params[:ref], message)
-
- if result[:status] == :success
- present result[:tag],
- with: Entities::RepoTag,
- project: user_project
- else
- render_api_error!(result[:message], 400)
- end
- end
-
# Get a project repository tree
#
# Parameters:
@@ -133,7 +98,7 @@ module API
authorize! :download_code, user_project
begin
- file_path = ArchiveRepositoryService.new(
+ ArchiveRepositoryService.new(
user_project,
params[:sha],
params[:format]
@@ -141,17 +106,6 @@ module API
rescue
not_found!('File')
end
-
- if file_path && File.exists?(file_path)
- data = File.open(file_path, 'rb').read
- basename = File.basename(file_path)
- header['Content-Disposition'] = "attachment; filename=\"#{basename}\""
- content_type MIME::Types.type_for(file_path).first.content_type
- env['api.format'] = :binary
- present data
- else
- redirect request.fullpath
- end
end
# Compare two branches, tags or commits
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 6727e80ac1e..203f04a6259 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -57,7 +57,7 @@ module API
# GET /project/:id/services/gitlab-ci
#
get ':id/services/:service_slug' do
- present project_service
+ present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
end
end
end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
new file mode 100644
index 00000000000..47621f443e6
--- /dev/null
+++ b/lib/api/tags.rb
@@ -0,0 +1,86 @@
+module API
+ # Git Tags API
+ class Tags < Grape::API
+ before { authenticate! }
+ before { authorize! :download_code, user_project }
+
+ resource :projects do
+ # Get a project repository tags
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # GET /projects/:id/repository/tags
+ get ":id/repository/tags" do
+ present user_project.repo.tags.sort_by(&:name).reverse,
+ with: Entities::RepoTag, project: user_project
+ end
+
+ # Create tag
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # tag_name (required) - The name of the tag
+ # ref (required) - Create tag from commit sha or branch
+ # message (optional) - Specifying a message creates an annotated tag.
+ # Example Request:
+ # POST /projects/:id/repository/tags
+ post ':id/repository/tags' do
+ authorize_push_project
+ message = params[:message] || nil
+ result = CreateTagService.new(user_project, current_user).
+ execute(params[:tag_name], params[:ref], message, params[:release_description])
+
+ if result[:status] == :success
+ present result[:tag],
+ with: Entities::RepoTag,
+ project: user_project
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
+ # Add release notes to tag
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # tag_name (required) - The name of the tag
+ # description (required) - Release notes with markdown support
+ # Example Request:
+ # POST /projects/:id/repository/tags/:tag_name/release
+ post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.*/ } do
+ authorize_push_project
+ required_attributes! [:description]
+ result = CreateReleaseService.new(user_project, current_user).
+ execute(params[:tag_name], params[:description])
+
+ if result[:status] == :success
+ present result[:release], with: Entities::Release
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+
+ # Updates a release notes of a tag
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # tag_name (required) - The name of the tag
+ # description (required) - Release notes with markdown support
+ # Example Request:
+ # PUT /projects/:id/repository/tags/:tag_name/release
+ put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.*/ } do
+ authorize_push_project
+ required_attributes! [:description]
+ result = UpdateReleaseService.new(user_project, current_user).
+ execute(params[:tag_name], params[:description])
+
+ if result[:status] == :success
+ present result[:release], with: Entities::Release
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb
new file mode 100644
index 00000000000..d58a196c4ef
--- /dev/null
+++ b/lib/award_emoji.rb
@@ -0,0 +1,12 @@
+class AwardEmoji
+ EMOJI_LIST = [
+ "+1", "-1", "100", "blush", "heart", "smile", "rage",
+ "beers", "disappointed", "ok_hand",
+ "helicopter", "shit", "airplane", "alarm_clock",
+ "ambulance", "anguished", "two_hearts", "wink"
+ ]
+
+ def self.path_to_emoji_image(name)
+ "emoji/#{Emoji.emoji_filename(name)}.png"
+ end
+end
diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb
new file mode 100644
index 00000000000..51fa3867e67
--- /dev/null
+++ b/lib/backup/artifacts.rb
@@ -0,0 +1,13 @@
+require 'backup/files'
+
+module Backup
+ class Artifacts < Files
+ def initialize
+ super('artifacts', ArtifactUploader.artifacts_path)
+ end
+
+ def create_files_dir
+ Dir.mkdir(app_files_dir, 0700)
+ end
+ end
+end
diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb
index 6f56f680bb9..635967f4bd4 100644
--- a/lib/backup/builds.rb
+++ b/lib/backup/builds.rb
@@ -1,34 +1,13 @@
-module Backup
- class Builds
- attr_reader :app_builds_dir, :backup_builds_dir, :backup_dir
+require 'backup/files'
+module Backup
+ class Builds < Files
def initialize
- @app_builds_dir = Settings.gitlab_ci.builds_path
- @backup_dir = Gitlab.config.backup.path
- @backup_builds_dir = File.join(Gitlab.config.backup.path, 'builds')
- end
-
- # Copy builds from builds directory to backup/builds
- def dump
- FileUtils.rm_rf(backup_builds_dir)
- # Ensure the parent dir of backup_builds_dir exists
- FileUtils.mkdir_p(Gitlab.config.backup.path)
- # Fail if somebody raced to create backup_builds_dir before us
- FileUtils.mkdir(backup_builds_dir, mode: 0700)
- FileUtils.cp_r(app_builds_dir, backup_dir)
- end
-
- def restore
- backup_existing_builds_dir
-
- FileUtils.cp_r(backup_builds_dir, app_builds_dir)
+ super('builds', Settings.gitlab_ci.builds_path)
end
- def backup_existing_builds_dir
- timestamped_builds_path = File.join(app_builds_dir, '..', "builds.#{Time.now.to_i}")
- if File.exists?(app_builds_dir)
- FileUtils.mv(app_builds_dir, File.expand_path(timestamped_builds_path))
- end
+ def create_files_dir
+ Dir.mkdir(app_files_dir, 0700)
end
end
end
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index 959ac4b7868..67b2a64bd10 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -2,26 +2,26 @@ require 'yaml'
module Backup
class Database
- attr_reader :config, :db_dir
+ attr_reader :config, :db_file_name
def initialize
@config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env]
- @db_dir = File.join(Gitlab.config.backup.path, 'db')
+ @db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
end
def dump
- FileUtils.rm_rf(@db_dir)
- # Ensure the parent dir of @db_dir exists
- FileUtils.mkdir_p(Gitlab.config.backup.path)
- # Fail if somebody raced to create @db_dir before us
- FileUtils.mkdir(@db_dir, mode: 0700)
+ FileUtils.mkdir_p(File.dirname(db_file_name))
+ FileUtils.rm_f(db_file_name)
+ compress_rd, compress_wr = IO.pipe
+ compress_pid = spawn(*%W(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600])
+ compress_rd.close
- success = case config["adapter"]
+ dump_pid = case config["adapter"]
when /^mysql/ then
$progress.print "Dumping MySQL database #{config['database']} ... "
# Workaround warnings from MySQL 5.6 about passwords on cmd line
ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
- system('mysqldump', *mysql_args, config['database'], out: db_file_name)
+ spawn('mysqldump', *mysql_args, config['database'], out: compress_wr)
when "postgresql" then
$progress.print "Dumping PostgreSQL database #{config['database']} ... "
pg_env
@@ -30,48 +30,42 @@ module Backup
pgsql_args << "-n"
pgsql_args << Gitlab.config.backup.pg_schema
end
- system('pg_dump', *pgsql_args, config['database'], out: db_file_name)
+ spawn('pg_dump', *pgsql_args, config['database'], out: compress_wr)
end
- report_success(success)
- abort 'Backup failed' unless success
+ compress_wr.close
+
+ success = [compress_pid, dump_pid].all? { |pid| Process.waitpid(pid); $?.success? }
- $progress.print 'Compressing database ... '
- success = system('gzip', db_file_name)
report_success(success)
- abort 'Backup failed: compress error' unless success
+ abort 'Backup failed' unless success
end
def restore
- $progress.print 'Decompressing database ... '
- success = system('gzip', '-d', db_file_name_gz)
- report_success(success)
- abort 'Restore failed: decompress error' unless success
+ decompress_rd, decompress_wr = IO.pipe
+ decompress_pid = spawn(*%W(gzip -cd), out: decompress_wr, in: db_file_name)
+ decompress_wr.close
- success = case config["adapter"]
+ restore_pid = case config["adapter"]
when /^mysql/ then
$progress.print "Restoring MySQL database #{config['database']} ... "
# Workaround warnings from MySQL 5.6 about passwords on cmd line
ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
- system('mysql', *mysql_args, config['database'], in: db_file_name)
+ spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
when "postgresql" then
$progress.print "Restoring PostgreSQL database #{config['database']} ... "
pg_env
- system('psql', config['database'], '-f', db_file_name)
+ spawn('psql', config['database'], in: decompress_rd)
end
+ decompress_rd.close
+
+ success = [decompress_pid, restore_pid].all? { |pid| Process.waitpid(pid); $?.success? }
+
report_success(success)
abort 'Restore failed' unless success
end
protected
- def db_file_name
- File.join(db_dir, 'database.sql')
- end
-
- def db_file_name_gz
- File.join(db_dir, 'database.sql.gz')
- end
-
def mysql_args
args = {
'host' => '--host',
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
new file mode 100644
index 00000000000..654b4d1c896
--- /dev/null
+++ b/lib/backup/files.rb
@@ -0,0 +1,40 @@
+require 'open3'
+
+module Backup
+ class Files
+ attr_reader :name, :app_files_dir, :backup_tarball, :files_parent_dir
+
+ def initialize(name, app_files_dir)
+ @name = name
+ @app_files_dir = File.realpath(app_files_dir)
+ @files_parent_dir = File.realpath(File.join(@app_files_dir, '..'))
+ @backup_tarball = File.join(Gitlab.config.backup.path, name + '.tar.gz')
+ end
+
+ # Copy files from public/files to backup/files
+ def dump
+ FileUtils.mkdir_p(Gitlab.config.backup.path)
+ FileUtils.rm_f(backup_tarball)
+ run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+ end
+
+ def restore
+ backup_existing_files_dir
+ create_files_dir
+
+ run_pipeline!([%W(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball)
+ end
+
+ def backup_existing_files_dir
+ timestamped_files_path = File.join(files_parent_dir, "#{name}.#{Time.now.to_i}")
+ if File.exists?(app_files_dir)
+ FileUtils.mv(app_files_dir, File.expand_path(timestamped_files_path))
+ end
+ end
+
+ def run_pipeline!(cmd_list, options={})
+ status_list = Open3.pipeline(*cmd_list, options)
+ abort 'Backup failed' unless status_list.compact.all?(&:success?)
+ end
+ end
+end
diff --git a/lib/backup/lfs.rb b/lib/backup/lfs.rb
new file mode 100644
index 00000000000..4153467fbee
--- /dev/null
+++ b/lib/backup/lfs.rb
@@ -0,0 +1,13 @@
+require 'backup/files'
+
+module Backup
+ class Lfs < Files
+ def initialize
+ super('lfs', Settings.lfs.storage_path)
+ end
+
+ def create_files_dir
+ Dir.mkdir(app_files_dir, 0700)
+ end
+ end
+end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index ac63f89c6ec..099062eeb8b 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -45,7 +45,8 @@ module Backup
directory = connection.directories.get(remote_directory)
if directory.files.create(key: tar_file, body: File.open(tar_file), public: false,
- multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size)
+ multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
+ encryption: Gitlab.config.backup.upload.encryption)
$progress.puts "done".green
else
puts "uploading backup to #{remote_directory} failed".red
@@ -55,7 +56,7 @@ module Backup
def cleanup
$progress.print "Deleting tmp directories ... "
-
+
backup_contents.each do |dir|
next unless File.exist?(File.join(Gitlab.config.backup.path, dir))
@@ -75,7 +76,7 @@ module Backup
if keep_time > 0
removed = 0
-
+
Dir.chdir(Gitlab.config.backup.path) do
file_list = Dir.glob('*_gitlab_backup.tar')
file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ }
@@ -149,17 +150,15 @@ module Backup
private
def backup_contents
- folders_to_backup + ["backup_information.yml"]
+ folders_to_backup + archives_to_backup + ["backup_information.yml"]
end
- def folders_to_backup
- folders = %w{repositories db uploads builds}
-
- if ENV["SKIP"]
- return folders.reject{ |folder| ENV["SKIP"].include?(folder) }
- end
+ def archives_to_backup
+ %w{uploads builds artifacts lfs}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact
+ end
- folders
+ def folders_to_backup
+ %w{repositories db}.reject{ |name| skipped?(name) }
end
def settings
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 4d70f7883dd..a82a7e1f7bf 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -35,7 +35,7 @@ module Backup
if wiki.repository.empty?
$progress.puts " [SKIPPED]".cyan
else
- cmd = %W(git --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all)
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all)
output, status = Gitlab::Popen.popen(cmd)
if status.zero?
$progress.puts " [DONE]".green
@@ -67,7 +67,7 @@ module Backup
FileUtils.mkdir_p(path_to_repo(project))
cmd = %W(tar -xf #{path_to_bundle(project)} -C #{path_to_repo(project)})
else
- cmd = %W(git init --bare #{path_to_repo(project)})
+ cmd = %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_repo(project)})
end
if system(*cmd, silent)
@@ -87,7 +87,7 @@ module Backup
# that was initialized with ProjectWiki.new() and then
# try to restore with 'git clone --bare'.
FileUtils.rm_rf(path_to_repo(wiki))
- cmd = %W(git clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)})
+ cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)})
if system(*cmd, silent)
$progress.puts " [DONE]".green
diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb
index 1f9626644e6..9261f77f3c9 100644
--- a/lib/backup/uploads.rb
+++ b/lib/backup/uploads.rb
@@ -1,34 +1,14 @@
+require 'backup/files'
+
module Backup
- class Uploads
- attr_reader :app_uploads_dir, :backup_uploads_dir, :backup_dir
+ class Uploads < Files
def initialize
- @app_uploads_dir = File.realpath(Rails.root.join('public', 'uploads'))
- @backup_dir = Gitlab.config.backup.path
- @backup_uploads_dir = File.join(Gitlab.config.backup.path, 'uploads')
- end
-
- # Copy uploads from public/uploads to backup/uploads
- def dump
- FileUtils.rm_rf(backup_uploads_dir)
- # Ensure the parent dir of backup_uploads_dir exists
- FileUtils.mkdir_p(Gitlab.config.backup.path)
- # Fail if somebody raced to create backup_uploads_dir before us
- FileUtils.mkdir(backup_uploads_dir, mode: 0700)
- FileUtils.cp_r(app_uploads_dir, backup_dir)
- end
-
- def restore
- backup_existing_uploads_dir
-
- FileUtils.cp_r(backup_uploads_dir, app_uploads_dir)
+ super('uploads', Rails.root.join('public/uploads'))
end
- def backup_existing_uploads_dir
- timestamped_uploads_path = File.join(app_uploads_dir, '..', "uploads.#{Time.now.to_i}")
- if File.exists?(app_uploads_dir)
- FileUtils.mv(app_uploads_dir, File.expand_path(timestamped_uploads_path))
- end
+ def create_files_dir
+ Dir.mkdir(app_files_dir)
end
end
end
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
index 5109c84e0ea..07e68216d7f 100644
--- a/lib/ci/api/api.rb
+++ b/lib/ci/api/api.rb
@@ -23,14 +23,11 @@ module Ci
rack_response({ 'message' => '500 Internal Server Error' }, 500)
end
- before do
- check_enable_flag!
- end
-
format :json
helpers Helpers
- helpers ::API::APIHelpers
+ helpers ::API::Helpers
+ helpers Gitlab::CurrentSettings
mount Builds
mount Commits
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 83ca1e6481c..15faa6edd84 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -47,6 +47,108 @@ module Ci
build.drop
end
end
+
+ # Authorize artifacts uploading for build - Runners only
+ #
+ # Parameters:
+ # id (required) - The ID of a build
+ # token (required) - The build authorization token
+ # filesize (optional) - the size of uploaded file
+ # Example Request:
+ # POST /builds/:id/artifacts/authorize
+ post ":id/artifacts/authorize" do
+ require_gitlab_workhorse!
+ not_allowed! unless Gitlab.config.artifacts.enabled
+ build = Ci::Build.find_by_id(params[:id])
+ not_found! unless build
+ authenticate_build_token!(build)
+ forbidden!('build is not running') unless build.running?
+
+ if params[:filesize]
+ file_size = params[:filesize].to_i
+ file_to_large! unless file_size < max_artifacts_size
+ end
+
+ status 200
+ { TempPath: ArtifactUploader.artifacts_upload_path }
+ end
+
+ # Upload artifacts to build - Runners only
+ #
+ # Parameters:
+ # id (required) - The ID of a build
+ # token (required) - The build authorization token
+ # file (required) - The uploaded file
+ # Parameters (accelerated by GitLab Workhorse):
+ # file.path - path to locally stored body (generated by Workhorse)
+ # file.name - real filename as send in Content-Disposition
+ # file.type - real content type as send in Content-Type
+ # Headers:
+ # BUILD-TOKEN (required) - The build authorization token, the same as token
+ # Body:
+ # The file content
+ #
+ # Example Request:
+ # POST /builds/:id/artifacts
+ post ":id/artifacts" do
+ require_gitlab_workhorse!
+ not_allowed! unless Gitlab.config.artifacts.enabled
+ build = Ci::Build.find_by_id(params[:id])
+ not_found! unless build
+ authenticate_build_token!(build)
+ forbidden!('build is not running') unless build.running?
+
+ file = uploaded_file!(:file, ArtifactUploader.artifacts_upload_path)
+ file_to_large! unless file.size < max_artifacts_size
+
+ if build.update_attributes(artifacts_file: file)
+ present build, with: Entities::Build
+ else
+ render_validation_error!(build)
+ end
+ end
+
+ # Download the artifacts file from build - Runners only
+ #
+ # Parameters:
+ # id (required) - The ID of a build
+ # token (required) - The build authorization token
+ # Headers:
+ # BUILD-TOKEN (required) - The build authorization token, the same as token
+ # Example Request:
+ # GET /builds/:id/artifacts
+ get ":id/artifacts" do
+ build = Ci::Build.find_by_id(params[:id])
+ not_found! unless build
+ authenticate_build_token!(build)
+ artifacts_file = build.artifacts_file
+
+ unless artifacts_file.file_storage?
+ return redirect_to build.artifacts_file.url
+ end
+
+ unless artifacts_file.exists?
+ not_found!
+ end
+
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ end
+
+ # Remove the artifacts file from build
+ #
+ # Parameters:
+ # id (required) - The ID of a build
+ # token (required) - The build authorization token
+ # Headers:
+ # BUILD-TOKEN (required) - The build authorization token, the same as token
+ # Example Request:
+ # DELETE /builds/:id/artifacts
+ delete ":id/artifacts" do
+ build = Ci::Build.find_by_id(params[:id])
+ not_found! unless build
+ authenticate_build_token!(build)
+ build.remove_artifacts_file!
+ end
end
end
end
diff --git a/lib/ci/api/commits.rb b/lib/ci/api/commits.rb
index bac463a5909..a60769d8305 100644
--- a/lib/ci/api/commits.rb
+++ b/lib/ci/api/commits.rb
@@ -51,7 +51,7 @@ module Ci
required_attributes! [:project_id, :data, :project_token]
project = Ci::Project.find(params[:project_id])
authenticate_project_token!(project)
- commit = Ci::CreateCommitService.new.execute(project, params[:data])
+ commit = Ci::CreateCommitService.new.execute(project, current_user, params[:data])
if commit.persisted?
present commit, with: Entities::CommitWithBuilds
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
index f47bc1236b8..750f421872d 100644
--- a/lib/ci/api/entities.rb
+++ b/lib/ci/api/entities.rb
@@ -2,7 +2,7 @@ module Ci
module API
module Entities
class Commit < Grape::Entity
- expose :id, :ref, :sha, :project_id, :before_sha, :created_at
+ expose :id, :sha, :project_id, :created_at
expose :status, :finished_at, :duration
expose :git_commit_message, :git_author_name, :git_author_email
end
@@ -11,10 +11,16 @@ module Ci
expose :builds
end
+ class ArtifactFile < Grape::Entity
+ expose :filename, :size
+ end
+
class Build < Grape::Entity
- expose :id, :commands, :ref, :sha, :project_id, :repo_url,
+ expose :id, :commands, :ref, :sha, :status, :project_id, :repo_url,
:before_sha, :allow_git_fetch, :project_name
+ expose :name, :token, :stage
+
expose :options do |model|
model.options
end
@@ -24,6 +30,7 @@ module Ci
end
expose :variables
+ expose :artifacts_file, using: ArtifactFile
end
class Runner < Grape::Entity
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 8e893aa5cc6..02502333756 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -1,14 +1,10 @@
module Ci
module API
module Helpers
+ BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN"
+ BUILD_TOKEN_PARAM = :token
UPDATE_RUNNER_EVERY = 60
- def check_enable_flag!
- unless current_application_settings.ci_enabled
- render_api_error!('400 (Bad request) CI is disabled', 400)
- end
- end
-
def authenticate_runners!
forbidden! unless params[:token] == GitlabCi::REGISTRATION_TOKEN
end
@@ -21,8 +17,15 @@ module Ci
forbidden! unless project.valid_token?(params[:project_token])
end
+ def authenticate_build_token!(build)
+ token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s
+ forbidden! unless token && build.valid_token?(token)
+ end
+
def update_runner_last_contact
- if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= UPDATE_RUNNER_EVERY
+ # Use a random threshold to prevent beating DB updates
+ contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY)
+ if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= contacted_at_max_age
current_runner.update_attributes(contacted_at: Time.now)
end
end
@@ -36,6 +39,10 @@ module Ci
info = attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
current_runner.update(info)
end
+
+ def max_artifacts_size
+ current_application_settings.max_artifacts_size.megabytes.to_i
+ end
end
end
end
diff --git a/lib/ci/api/projects.rb b/lib/ci/api/projects.rb
index 66bcf65e8c4..d719ad9e8d5 100644
--- a/lib/ci/api/projects.rb
+++ b/lib/ci/api/projects.rb
@@ -75,23 +75,17 @@ module Ci
# Create Gitlab CI project using Gitlab project info
#
# Parameters:
- # name (required) - The name of the project
# gitlab_id (required) - The gitlab id of the project
- # path (required) - The gitlab project path, ex. randx/six
- # ssh_url_to_repo (required) - The gitlab ssh url to the repo
# default_ref - The branch to run against (defaults to `master`)
# Example Request:
# POST /projects
post do
- required_attributes! [:name, :gitlab_id, :ssh_url_to_repo]
+ required_attributes! [:gitlab_id]
filtered_params = {
- name: params[:name],
gitlab_id: params[:gitlab_id],
# we accept gitlab_url for backward compatibility for a while (added to 7.11)
- path: params[:path] || params[:gitlab_url].sub(/.*\/(.*\/.*)$/, '\1'),
- default_ref: params[:default_ref] || 'master',
- ssh_url_to_repo: params[:ssh_url_to_repo]
+ default_ref: params[:default_ref] || 'master'
}
project = Ci::Project.new(filtered_params)
@@ -109,11 +103,7 @@ module Ci
#
# Parameters:
# id (required) - The ID of a project
- # name - The name of the project
- # gitlab_id - The gitlab id of the project
- # path - The gitlab project path, ex. randx/six
- # ssh_url_to_repo - The gitlab ssh url to the repo
- # default_ref - The branch to run against (defaults to `master`)
+ # default_ref - The branch to run against (defaults to `master`)
# Example Request:
# PUT /projects/:id
put ":id" do
@@ -121,12 +111,7 @@ module Ci
unauthorized! unless can?(current_user, :admin_project, project.gl_project)
- attrs = attributes_for_keys [:name, :gitlab_id, :path, :gitlab_url, :default_ref, :ssh_url_to_repo]
-
- # we accept gitlab_url for backward compatibility for a while (added to 7.11)
- if attrs[:gitlab_url] && !attrs[:path]
- attrs[:path] = attrs[:gitlab_url].sub(/.*\/(.*\/.*)$/, '\1')
- end
+ attrs = attributes_for_keys [:default_ref]
if project.update_attributes(attrs)
present project, with: Entities::Project
diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb
index 915a4f526a6..5ff7407c6fe 100644
--- a/lib/ci/charts.rb
+++ b/lib/ci/charts.rb
@@ -60,7 +60,8 @@ module Ci
class BuildTime < Chart
def collect
- commits = project.commits.joins(:builds).where("#{Ci::Build.table_name}.finished_at is NOT NULL AND #{Ci::Build.table_name}.started_at is NOT NULL").last(30)
+ commits = project.commits.last(30)
+
commits.each do |commit|
@labels << commit.short_sha
@build_times << (commit.duration / 60)
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index e625e790df8..3beafcad117 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -4,13 +4,14 @@ module Ci
DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test'
- ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables]
- ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage]
+ ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache]
+ ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when, :artifacts, :cache]
- attr_reader :before_script, :image, :services, :variables
+ attr_reader :before_script, :image, :services, :variables, :path, :cache
- def initialize(config)
+ def initialize(config, path = nil)
@config = YAML.load(config)
+ @path = path
unless @config.is_a? Hash
raise ValidationError, "YAML should be a hash"
@@ -45,6 +46,7 @@ module Ci
@services = @config[:services]
@stages = @config[:stages] || @config[:types]
@variables = @config[:variables] || {}
+ @cache = @config[:cache]
@config.except!(*ALLOWED_YAML_KEYS)
# anything that doesn't have script is considered as unknown
@@ -63,50 +65,26 @@ module Ci
end
end
- def process?(only_params, except_params, ref, tag)
- return true if only_params.nil? && except_params.nil?
-
- if only_params
- return true if tag && only_params.include?("tags")
- return true if !tag && only_params.include?("branches")
-
- only_params.find do |pattern|
- match_ref?(pattern, ref)
- end
- else
- return false if tag && except_params.include?("tags")
- return false if !tag && except_params.include?("branches")
-
- except_params.each do |pattern|
- return false if match_ref?(pattern, ref)
- end
- end
- end
-
def build_job(name, job)
{
+ stage_idx: stages.index(job[:stage]),
stage: job[:stage],
- script: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}",
- tags: job[:tags] || [],
+ commands: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}",
+ tag_list: job[:tags] || [],
name: name,
only: job[:only],
except: job[:except],
allow_failure: job[:allow_failure] || false,
+ when: job[:when] || 'on_success',
options: {
image: job[:image] || @image,
- services: job[:services] || @services
+ services: job[:services] || @services,
+ artifacts: job[:artifacts],
+ cache: job[:cache] || @cache,
}.compact
}
end
- def match_ref?(pattern, ref)
- if pattern.first == "/" && pattern.last == "/"
- Regexp.new(pattern[1...-1]) =~ ref
- else
- pattern == ref
- end
- end
-
def normalize_script(script)
if script.is_a? Array
script.join("\n")
@@ -136,63 +114,140 @@ module Ci
raise ValidationError, "variables should be a map of key-valued strings"
end
+ if @cache
+ if @cache[:untracked] && !validate_boolean(@cache[:untracked])
+ raise ValidationError, "cache:untracked parameter should be an boolean"
+ end
+
+ if @cache[:paths] && !validate_array_of_strings(@cache[:paths])
+ raise ValidationError, "cache:paths parameter should be an array of strings"
+ end
+ end
+
@jobs.each do |name, job|
- validate_job!("#{name} job", job)
+ validate_job!(name, job)
end
true
end
def validate_job!(name, job)
+ if name.blank? || !validate_string(name)
+ raise ValidationError, "job name should be non-empty string"
+ end
+
job.keys.each do |key|
unless ALLOWED_JOB_KEYS.include? key
- raise ValidationError, "#{name}: unknown parameter #{key}"
+ raise ValidationError, "#{name} job: unknown parameter #{key}"
end
end
- if !job[:script].is_a?(String) && !validate_array_of_strings(job[:script])
- raise ValidationError, "#{name}: script should be a string or an array of a strings"
+ if !validate_string(job[:script]) && !validate_array_of_strings(job[:script])
+ raise ValidationError, "#{name} job: script should be a string or an array of a strings"
end
if job[:stage]
unless job[:stage].is_a?(String) && job[:stage].in?(stages)
- raise ValidationError, "#{name}: stage parameter should be #{stages.join(", ")}"
+ raise ValidationError, "#{name} job: stage parameter should be #{stages.join(", ")}"
end
end
- if job[:image] && !job[:image].is_a?(String)
- raise ValidationError, "#{name}: image should be a string"
+ if job[:image] && !validate_string(job[:image])
+ raise ValidationError, "#{name} job: image should be a string"
end
if job[:services] && !validate_array_of_strings(job[:services])
- raise ValidationError, "#{name}: services should be an array of strings"
+ raise ValidationError, "#{name} job: services should be an array of strings"
end
if job[:tags] && !validate_array_of_strings(job[:tags])
- raise ValidationError, "#{name}: tags parameter should be an array of strings"
+ raise ValidationError, "#{name} job: tags parameter should be an array of strings"
end
if job[:only] && !validate_array_of_strings(job[:only])
- raise ValidationError, "#{name}: only parameter should be an array of strings"
+ raise ValidationError, "#{name} job: only parameter should be an array of strings"
end
if job[:except] && !validate_array_of_strings(job[:except])
- raise ValidationError, "#{name}: except parameter should be an array of strings"
+ raise ValidationError, "#{name} job: except parameter should be an array of strings"
end
- if job[:allow_failure] && !job[:allow_failure].in?([true, false])
- raise ValidationError, "#{name}: allow_failure parameter should be an boolean"
+ if job[:cache]
+ if job[:cache][:untracked] && !validate_boolean(job[:cache][:untracked])
+ raise ValidationError, "#{name} job: cache:untracked parameter should be an boolean"
+ end
+
+ if job[:cache][:paths] && !validate_array_of_strings(job[:cache][:paths])
+ raise ValidationError, "#{name} job: cache:paths parameter should be an array of strings"
+ end
+ end
+
+ if job[:artifacts]
+ if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked])
+ raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean"
+ end
+
+ if job[:artifacts][:paths] && !validate_array_of_strings(job[:artifacts][:paths])
+ raise ValidationError, "#{name} job: artifacts:paths parameter should be an array of strings"
+ end
+ end
+
+ if job[:allow_failure] && !validate_boolean(job[:allow_failure])
+ raise ValidationError, "#{name} job: allow_failure parameter should be an boolean"
+ end
+
+ if job[:when] && !job[:when].in?(%w(on_success on_failure always))
+ raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always"
end
end
private
def validate_array_of_strings(values)
- values.is_a?(Array) && values.all? {|tag| tag.is_a?(String)}
+ values.is_a?(Array) && values.all? { |value| validate_string(value) }
end
def validate_variables(variables)
- variables.is_a?(Hash) && variables.all? {|key, value| key.is_a?(Symbol) && value.is_a?(String)}
+ variables.is_a?(Hash) && variables.all? { |key, value| validate_string(key) && validate_string(value) }
+ end
+
+ def validate_string(value)
+ value.is_a?(String) || value.is_a?(Symbol)
+ end
+
+ def validate_boolean(value)
+ value.in?([true, false])
+ end
+
+ def process?(only_params, except_params, ref, tag)
+ if only_params.present?
+ return false unless matching?(only_params, ref, tag)
+ end
+
+ if except_params.present?
+ return false if matching?(except_params, ref, tag)
+ end
+
+ true
+ end
+
+ def matching?(patterns, ref, tag)
+ patterns.any? do |pattern|
+ match_ref?(pattern, ref, tag)
+ end
+ end
+
+ def match_ref?(pattern, ref, tag)
+ pattern, path = pattern.split('@', 2)
+ return false if path && path != self.path
+ return true if tag && pattern == 'tags'
+ return true if !tag && pattern == 'branches'
+
+ if pattern.first == "/" && pattern.last == "/"
+ Regexp.new(pattern[1...-1]) =~ ref
+ else
+ pattern == ref
+ end
end
end
end
diff --git a/lib/ci/migrate/builds.rb b/lib/ci/migrate/builds.rb
deleted file mode 100644
index c4f62e55295..00000000000
--- a/lib/ci/migrate/builds.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module Ci
- module Migrate
- class Builds
- attr_reader :app_builds_dir, :backup_builds_tarball, :backup_dir
-
- def initialize
- @app_builds_dir = Settings.gitlab_ci.builds_path
- @backup_dir = Gitlab.config.backup.path
- @backup_builds_tarball = File.join(backup_dir, 'builds/builds.tar.gz')
- end
-
- def restore
- backup_existing_builds_dir
-
- FileUtils.mkdir_p(app_builds_dir, mode: 0700)
- unless system('tar', '-C', app_builds_dir, '-zxf', backup_builds_tarball)
- abort 'Restore failed'.red
- end
- end
-
- def backup_existing_builds_dir
- timestamped_builds_path = File.join(app_builds_dir, '..', "builds.#{Time.now.to_i}")
- if File.exists?(app_builds_dir)
- FileUtils.mv(app_builds_dir, File.expand_path(timestamped_builds_path))
- end
- end
- end
- end
-end
diff --git a/lib/ci/migrate/database.rb b/lib/ci/migrate/database.rb
deleted file mode 100644
index bf9b80f1f62..00000000000
--- a/lib/ci/migrate/database.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'yaml'
-
-module Ci
- module Migrate
- class Database
- attr_reader :config
-
- def initialize
- @config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env]
- end
-
- def restore
- decompress_rd, decompress_wr = IO.pipe
- decompress_pid = spawn(*%W(gzip -cd), out: decompress_wr, in: db_file_name)
- decompress_wr.close
-
- restore_pid = case config["adapter"]
- when /^mysql/ then
- $progress.print "Restoring MySQL database #{config['database']} ... "
- # Workaround warnings from MySQL 5.6 about passwords on cmd line
- ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
- spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
- when "postgresql" then
- $progress.print "Restoring PostgreSQL database #{config['database']} ... "
- pg_env
- spawn('psql', config['database'], in: decompress_rd)
- end
- decompress_rd.close
-
- success = [decompress_pid, restore_pid].all? { |pid| Process.waitpid(pid); $?.success? }
- abort 'Restore failed' unless success
- end
-
- protected
-
- def db_file_name
- File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
- end
-
- def mysql_args
- args = {
- 'host' => '--host',
- 'port' => '--port',
- 'socket' => '--socket',
- 'username' => '--user',
- 'encoding' => '--default-character-set'
- }
- args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact
- end
-
- def pg_env
- ENV['PGUSER'] = config["username"] if config["username"]
- ENV['PGHOST'] = config["host"] if config["host"]
- ENV['PGPORT'] = config["port"].to_s if config["port"]
- ENV['PGPASSWORD'] = config["password"].to_s if config["password"]
- end
-
- def report_success(success)
- if success
- puts '[DONE]'.green
- else
- puts '[FAILED]'.red
- end
- end
- end
- end
-end
diff --git a/lib/ci/migrate/manager.rb b/lib/ci/migrate/manager.rb
deleted file mode 100644
index e5e4fb784eb..00000000000
--- a/lib/ci/migrate/manager.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-module Ci
- module Migrate
- class Manager
- CI_IMPORT_PREFIX = '8.0' # Only allow imports from CI 8.0.x
-
- def cleanup
- $progress.print "Deleting tmp directories ... "
-
- backup_contents.each do |dir|
- next unless File.exist?(File.join(Gitlab.config.backup.path, dir))
-
- if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir))
- $progress.puts "done".green
- else
- puts "deleting tmp directory '#{dir}' failed".red
- abort 'Backup failed'
- end
- end
- end
-
- def unpack
- Dir.chdir(Gitlab.config.backup.path)
-
- # check for existing backups in the backup dir
- file_list = Dir.glob("*_gitlab_ci_backup.tar").each.map { |f| f.split(/_/).first.to_i }
- puts "no backups found" if file_list.count == 0
-
- if file_list.count > 1 && ENV["BACKUP"].nil?
- puts "Found more than one backup, please specify which one you want to restore:"
- puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup"
- exit 1
- end
-
- tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_ci_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_ci_backup.tar")
-
- unless File.exists?(tar_file)
- puts "The specified CI backup doesn't exist!"
- exit 1
- end
-
- $progress.print "Unpacking backup ... "
-
- unless Kernel.system(*%W(tar -xf #{tar_file}))
- puts "unpacking backup failed".red
- exit 1
- else
- $progress.puts "done".green
- end
-
- ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
-
- # restoring mismatching backups can lead to unexpected problems
- if !settings[:gitlab_version].start_with?(CI_IMPORT_PREFIX)
- puts "GitLab CI version mismatch:".red
- puts " Your current GitLab CI version (#{GitlabCi::VERSION}) differs from the GitLab CI (#{settings[:gitlab_version]}) version in the backup!".red
- exit 1
- end
- end
-
- private
-
- def backup_contents
- ["db", "builds", "backup_information.yml"]
- end
-
- def settings
- @settings ||= YAML.load_file("backup_information.yml")
- end
- end
- end
-end
-
diff --git a/lib/ci/migrate/tags.rb b/lib/ci/migrate/tags.rb
deleted file mode 100644
index 97e043ece27..00000000000
--- a/lib/ci/migrate/tags.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-require 'yaml'
-
-module Ci
- module Migrate
- class Tags
- def restore
- puts 'Inserting tags...'
- connection.select_all('SELECT ci_tags.name FROM ci_tags').each do |tag|
- begin
- connection.execute("INSERT INTO tags (name) VALUES(#{ActiveRecord::Base::sanitize(tag['name'])})")
- rescue ActiveRecord::RecordNotUnique
- end
- end
-
- ActiveRecord::Base.transaction do
- puts 'Deleting old taggings...'
- connection.execute "DELETE FROM taggings WHERE context = 'tags' AND taggable_type LIKE 'Ci::%'"
-
- puts 'Inserting taggings...'
- connection.execute(
- 'INSERT INTO taggings (taggable_type, taggable_id, tag_id, context) ' +
- "SELECT CONCAT('Ci::', ci_taggings.taggable_type), ci_taggings.taggable_id, tags.id, 'tags' FROM ci_taggings " +
- 'JOIN ci_tags ON ci_tags.id = ci_taggings.tag_id ' +
- 'JOIN tags ON tags.name = ci_tags.name '
- )
-
- puts 'Resetting counters... '
- connection.execute(
- 'UPDATE tags SET ' +
- 'taggings_count = (SELECT COUNT(*) FROM taggings WHERE tags.id = taggings.tag_id)'
- )
- end
- end
-
- protected
-
- def connection
- ActiveRecord::Base.connection
- end
- end
- end
-end
diff --git a/lib/ci/status.rb b/lib/ci/status.rb
new file mode 100644
index 00000000000..c02b3b8f3e4
--- /dev/null
+++ b/lib/ci/status.rb
@@ -0,0 +1,21 @@
+module Ci
+ class Status
+ def self.get_status(statuses)
+ statuses.reject! { |status| status.try(&:allow_failure?) }
+
+ if statuses.none?
+ 'skipped'
+ elsif statuses.all?(&:success?)
+ 'success'
+ elsif statuses.all?(&:pending?)
+ 'pending'
+ elsif statuses.any?(&:running?) || statuses.any?(&:pending?)
+ 'running'
+ elsif statuses.all?(&:canceled?)
+ 'canceled'
+ else
+ 'failed'
+ end
+ end
+ end
+end
diff --git a/lib/event_filter.rb b/lib/event_filter.rb
index 163937c02cf..f15b2cfd231 100644
--- a/lib/event_filter.rb
+++ b/lib/event_filter.rb
@@ -47,7 +47,7 @@ class EventFilter
actions << Event::COMMENTED if filter.include? 'comments'
- events = events.where(action: actions)
+ events.where(action: actions)
end
def options(key)
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 322aed5e27c..51e46da82cc 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -110,7 +110,7 @@ module ExtractsPath
@project, @ref, @path)
rescue RuntimeError, NoMethodError, InvalidPathError
- not_found!
+ render_404
end
def tree
diff --git a/lib/file_streamer.rb b/lib/file_streamer.rb
new file mode 100644
index 00000000000..4e3c6d3c773
--- /dev/null
+++ b/lib/file_streamer.rb
@@ -0,0 +1,16 @@
+class FileStreamer #:nodoc:
+ attr_reader :to_path
+
+ def initialize(path)
+ @to_path = path
+ end
+
+ # Stream the file's contents if Rack::Sendfile isn't present.
+ def each
+ File.open(to_path, 'rb') do |file|
+ while chunk = file.read(16384)
+ yield chunk
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index 0353b3b7ed3..0d156047ff0 100644
--- a/lib/gitlab/backend/grack_auth.rb
+++ b/lib/gitlab/backend/grack_auth.rb
@@ -33,8 +33,11 @@ module Grack
auth!
+ lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call
+ return lfs_response unless lfs_response.nil?
+
if project && authorized_request?
- # Tell gitlab-git-http-server the request is OK, and what the GL_ID is
+ # Tell gitlab-workhorse the request is OK, and what the GL_ID is
render_grack_auth_ok
elsif @user.nil? && !@ci
unauthorized
@@ -72,7 +75,7 @@ module Grack
matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
if project && matched_login.present? && git_cmd == 'git-upload-pack'
- underscored_service = matched_login['s'].underscore
+ underscored_service = matched_login['s'].underscore
if Service.available_services_names.include?(underscored_service)
service_method = "#{underscored_service}_service"
@@ -193,7 +196,21 @@ module Grack
end
def render_grack_auth_ok
- [200, { "Content-Type" => "application/json" }, [JSON.dump({ 'GL_ID' => Gitlab::ShellEnv.gl_id(@user) })]]
+ repo_path =
+ if @request.path_info =~ /^([\w\.\/-]+)\.wiki\.git/
+ ProjectWiki.new(project).repository.path_to_repo
+ else
+ project.repository.path_to_repo
+ end
+
+ [
+ 200,
+ { "Content-Type" => "application/json" },
+ [JSON.dump({
+ 'GL_ID' => Gitlab::ShellEnv.gl_id(@user),
+ 'RepoPath' => repo_path,
+ })]
+ ]
end
def render_not_found
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index 14ee4701e7b..87ac30b5ffe 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -1,10 +1,11 @@
module Gitlab
class Shell
- class AccessDenied < StandardError; end
+ class Error < StandardError; end
class KeyAdder < Struct.new(:io)
def add_key(id, key)
- io.puts("#{id}\t#{key.strip}")
+ key.gsub!(/[[:space:]]+/, ' ').strip!
+ io.puts("#{id}\t#{key}")
end
end
@@ -35,8 +36,9 @@ module Gitlab
# import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git")
#
def import_repository(name, url)
- Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'import-project',
- "#{name}.git", url, '240'])
+ output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', "#{name}.git", url, '240'])
+ raise Error, output unless status.zero?
+ true
end
# Move repository
diff --git a/lib/gitlab/blacklist.rb b/lib/gitlab/blacklist.rb
deleted file mode 100644
index 43145e0ee1b..00000000000
--- a/lib/gitlab/blacklist.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module Gitlab
- module Blacklist
- extend self
-
- def path
- %w(
- admin
- dashboard
- files
- groups
- help
- profile
- projects
- search
- public
- assets
- u
- s
- teams
- merge_requests
- issues
- users
- snippets
- services
- repository
- hooks
- notes
- unsubscribes
- all
- ci
- )
- end
- end
-end
diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb
index aeec595782c..9bef9037ad6 100644
--- a/lib/gitlab/closing_issue_extractor.rb
+++ b/lib/gitlab/closing_issue_extractor.rb
@@ -1,6 +1,12 @@
module Gitlab
class ClosingIssueExtractor
- ISSUE_CLOSING_REGEX = Regexp.new(Gitlab.config.gitlab.issue_closing_pattern)
+ ISSUE_CLOSING_REGEX = begin
+ link_pattern = URI.regexp(%w(http https))
+
+ pattern = Gitlab.config.gitlab.issue_closing_pattern
+ pattern = pattern.sub('%{issue_ref}', "(?:(?:#{link_pattern})|(?:#{Issue.reference_pattern}))")
+ Regexp.new(pattern).freeze
+ end
def initialize(project, current_user = nil)
@extractor = Gitlab::ReferenceExtractor.new(project, current_user)
@@ -9,10 +15,12 @@ module Gitlab
def closed_by_message(message)
return [] if message.nil?
- closing_statements = message.scan(ISSUE_CLOSING_REGEX).
- map { |ref| ref[0] }.join(" ")
+ closing_statements = []
+ message.scan(ISSUE_CLOSING_REGEX) do
+ closing_statements << Regexp.last_match[0]
+ end
- @extractor.analyze(closing_statements)
+ @extractor.analyze(closing_statements.join(" "))
@extractor.issues
end
diff --git a/lib/gitlab/compare_result.rb b/lib/gitlab/compare_result.rb
index d72391dade5..0d696a1ee28 100644
--- a/lib/gitlab/compare_result.rb
+++ b/lib/gitlab/compare_result.rb
@@ -2,8 +2,8 @@ module Gitlab
class CompareResult
attr_reader :commits, :diffs
- def initialize(compare)
- @commits, @diffs = compare.commits, compare.diffs
+ def initialize(compare, diff_options = {})
+ @commits, @diffs = compare.commits, compare.diffs(nil, diff_options)
end
end
end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 45bb904ed7a..8a7f8dc5003 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -12,7 +12,6 @@ module Gitlab
@timestamps = {}
date_from = 1.year.ago
- date_to = Date.today
events = Event.reorder(nil).contributions.where(author_id: user.id).
where("created_at > ?", date_from).where(project_id: projects).
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 0ea1b6a2f6f..46a4ef0e31f 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -23,7 +23,9 @@ module Gitlab
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
- import_sources: Settings.gitlab['import_sources']
+ import_sources: Settings.gitlab['import_sources'],
+ shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
+ max_artifacts_size: Settings.artifacts['max_size'],
)
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
new file mode 100644
index 00000000000..71f37f1fef8
--- /dev/null
+++ b/lib/gitlab/database.rb
@@ -0,0 +1,11 @@
+module Gitlab
+ module Database
+ def self.mysql?
+ ActiveRecord::Base.connection.adapter_name.downcase == 'mysql2'
+ end
+
+ def self.postgresql?
+ ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql'
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 4daf65331e8..142058aa69d 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -44,6 +44,14 @@ module Gitlab
diff.old_path
end
end
+
+ def added_lines
+ diff_lines.select(&:added?).size
+ end
+
+ def removed_lines
+ diff_lines.select(&:removed?).size
+ end
end
end
end
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 8ac1b15e88a..0072194606e 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -7,6 +7,14 @@ module Gitlab
@text, @type, @index = text, type, index
@old_pos, @new_pos = old_pos, new_pos
end
+
+ def added?
+ type == 'new'
+ end
+
+ def removed?
+ type == 'old'
+ end
end
end
end
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index c1d9520ddf1..7015fe36c3d 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -14,8 +14,6 @@ module Gitlab
lines_arr = ::Gitlab::InlineDiff.processing lines
lines_arr.each do |line|
- raw_line = line.dup
-
next if filename?(line)
full_line = html_escape(line.gsub(/\n/, ''))
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 61e08b23543..496256700b8 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -154,7 +154,7 @@ module Gitlab
while comment = comments.shift
verb = comment['sVerb']
- next if verb == 'Opened' || verb === 'Closed'
+ next if verb == 'Opened'
content = format_content(comment['s'])
attachments = format_attachments(comment['rgAttachments'])
diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb
index f02ea43910f..8b1b6f48ed5 100644
--- a/lib/gitlab/fogbugz_import/project_creator.rb
+++ b/lib/gitlab/fogbugz_import/project_creator.rb
@@ -23,7 +23,7 @@ module Gitlab
import_url: Project::UNKNOWN_IMPORT_URL
).execute
- import_data = project.create_import_data(
+ project.create_import_data(
data: {
'repo' => repo.raw_data,
'user_map' => user_map,
diff --git a/lib/gitlab/force_push_check.rb b/lib/gitlab/force_push_check.rb
index fdb6a35c78d..93c6a5bb7f5 100644
--- a/lib/gitlab/force_push_check.rb
+++ b/lib/gitlab/force_push_check.rb
@@ -7,7 +7,7 @@ module Gitlab
if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
false
else
- missed_refs, _ = Gitlab::Popen.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev}))
+ missed_refs, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev}))
missed_refs.split("\n").size > 0
end
end
diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb
index dd393fe09d2..07b856ca64c 100644
--- a/lib/gitlab/git/hook.rb
+++ b/lib/gitlab/git/hook.rb
@@ -16,6 +16,17 @@ module Gitlab
def trigger(gl_id, oldrev, newrev, ref)
return true unless exists?
+ case name
+ when "pre-receive", "post-receive"
+ call_receive_hook(gl_id, oldrev, newrev, ref)
+ when "update"
+ call_update_hook(gl_id, oldrev, newrev, ref)
+ end
+ end
+
+ private
+
+ def call_receive_hook(gl_id, oldrev, newrev, ref)
changes = [oldrev, newrev, ref].join(" ")
# function will return true if succesful
@@ -54,6 +65,12 @@ module Gitlab
exit_status
end
+
+ def call_update_hook(gl_id, oldrev, newrev, ref)
+ Dir.chdir(repo_path) do
+ system({ 'GL_ID' => gl_id }, path, ref, oldrev, newrev)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index c90184d31cf..3ed1eec517c 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -13,7 +13,7 @@ module Gitlab
def user
return @user if defined?(@user)
- @user =
+ @user =
case actor
when User
actor
@@ -125,7 +125,7 @@ module Gitlab
def change_access_check(change)
oldrev, newrev, ref = change.split(' ')
- action =
+ action =
if project.protected_branch?(branch_name(ref))
protected_branch_action(oldrev, newrev, branch_name(ref))
elsif protected_tag?(tag_name(ref))
@@ -148,7 +148,7 @@ module Gitlab
build_status_object(false, "You are not allowed to change existing tags on this project.")
else # :push_code
build_status_object(false, "You are not allowed to push code to this project.")
- end
+ end
return status
end
diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb
index 39d17def930..4d83d8e72a8 100644
--- a/lib/gitlab/git_ref_validator.rb
+++ b/lib/gitlab/git_ref_validator.rb
@@ -6,7 +6,7 @@ module Gitlab
# Returns true for a valid reference name, false otherwise
def validate(ref_name)
Gitlab::Utils.system_silent(
- %W(git check-ref-format refs/#{ref_name}))
+ %W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name}))
end
end
end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 270cbcd9ccd..74d1529e1ff 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -46,7 +46,7 @@ module Gitlab
end
def github_options
- OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys
+ OmniAuth::Strategies::GitHub.default_options[:client_options].to_h.symbolize_keys
end
end
end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index bd7340a80f1..b5720f6e2cb 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -19,7 +19,7 @@ module Gitlab
if issue.pull_request.nil?
body = @formatter.author_line(issue.user.login)
- body += issue.body
+ body += issue.body || ""
if issue.comments > 0
body += @formatter.comments_header
diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb
index 9c00896c913..86fb6c51765 100644
--- a/lib/gitlab/gitlab_import/client.rb
+++ b/lib/gitlab/gitlab_import/client.rb
@@ -75,7 +75,7 @@ module Gitlab
end
def gitlab_options
- OmniAuth::Strategies::GitLab.default_options[:client_options].symbolize_keys
+ OmniAuth::Strategies::GitLab.default_options[:client_options].to_h.symbolize_keys
end
end
end
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 03c410726a5..87fee28dc01 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -30,7 +30,7 @@ module Gitlab
def user_map
@user_map ||= begin
- user_map = Hash.new do |hash, user|
+ user_map = Hash.new do |hash, user|
# Replace ... by \.\.\., so `johnsm...@gmail.com` isn't autolinked.
Client.mask_email(user).sub("...", "\\.\\.\\.")
end
@@ -76,18 +76,7 @@ module Gitlab
attachments = format_attachments(raw_issue["id"], 0, issue_comment["attachments"])
body = format_issue_body(author, date, content, attachments)
-
- labels = []
- raw_issue["labels"].each do |label|
- name = nice_label_name(label)
- labels << name
-
- unless @known_labels.include?(name)
- create_label(name)
- @known_labels << name
- end
- end
- labels << nice_status_name(raw_issue["status"])
+ labels = import_issue_labels(raw_issue)
assignee_id = nil
if raw_issue.has_key?("owner")
@@ -110,6 +99,7 @@ module Gitlab
assignee_id: assignee_id,
state: raw_issue["state"] == "closed" ? "closed" : "opened"
)
+
issue.add_labels_by_names(labels)
if issue.iid != raw_issue["id"]
@@ -120,6 +110,23 @@ module Gitlab
end
end
+ def import_issue_labels(raw_issue)
+ labels = []
+
+ raw_issue["labels"].each do |label|
+ name = nice_label_name(label)
+ labels << name
+
+ unless @known_labels.include?(name)
+ create_label(name)
+ @known_labels << name
+ end
+ end
+
+ labels << nice_status_name(raw_issue["status"])
+ labels
+ end
+
def import_issue_comments(issue, comments)
Note.transaction do
while raw_comment = comments.shift
@@ -172,7 +179,7 @@ module Gitlab
"#5cb85c"
when "Status: Started"
"#8e44ad"
-
+
when "Priority: Critical"
"#ffcfcf"
when "Priority: High"
@@ -181,7 +188,7 @@ module Gitlab
"#fff5cc"
when "Priority: Low"
"#cfe9ff"
-
+
when "Type: Defect"
"#d9534f"
when "Type: Enhancement"
@@ -249,8 +256,8 @@ module Gitlab
end
if raw_updates.has_key?("cc")
- cc = raw_updates["cc"].map do |l|
- deleted = l.start_with?("-")
+ cc = raw_updates["cc"].map do |l|
+ deleted = l.start_with?("-")
l = l[1..-1] if deleted
l = user_map[l]
l = "~~#{l}~~" if deleted
@@ -261,8 +268,8 @@ module Gitlab
end
if raw_updates.has_key?("labels")
- labels = raw_updates["labels"].map do |l|
- deleted = l.start_with?("-")
+ labels = raw_updates["labels"].map do |l|
+ deleted = l.start_with?("-")
l = l[1..-1] if deleted
l = nice_label_name(l)
l = "~~#{l}~~" if deleted
@@ -278,45 +285,39 @@ module Gitlab
if raw_updates.has_key?("blockedOn")
blocked_ons = raw_updates["blockedOn"].map do |raw_blocked_on|
- name, id = raw_blocked_on.split(":", 2)
-
- deleted = name.start_with?("-")
- name = name[1..-1] if deleted
-
- text =
- if name == project.import_source
- "##{id}"
- else
- "#{project.namespace.path}/#{name}##{id}"
- end
- text = "~~#{text}~~" if deleted
- text
+ format_blocking_updates(raw_blocked_on)
end
+
updates << "*Blocked on: #{blocked_ons.join(", ")}*"
end
if raw_updates.has_key?("blocking")
blockings = raw_updates["blocking"].map do |raw_blocked_on|
- name, id = raw_blocked_on.split(":", 2)
-
- deleted = name.start_with?("-")
- name = name[1..-1] if deleted
-
- text =
- if name == project.import_source
- "##{id}"
- else
- "#{project.namespace.path}/#{name}##{id}"
- end
- text = "~~#{text}~~" if deleted
- text
+ format_blocking_updates(raw_blocked_on)
end
+
updates << "*Blocking: #{blockings.join(", ")}*"
end
updates
end
+ def format_blocking_updates(raw_blocked_on)
+ name, id = raw_blocked_on.split(":", 2)
+
+ deleted = name.start_with?("-")
+ name = name[1..-1] if deleted
+
+ text =
+ if name == project.import_source
+ "##{id}"
+ else
+ "#{project.namespace.path}/#{name}##{id}"
+ end
+ text = "~~#{text}~~" if deleted
+ text
+ end
+
def format_attachments(issue_id, comment_id, raw_attachments)
return [] unless raw_attachments
@@ -325,7 +326,7 @@ module Gitlab
filename = attachment["fileName"]
link = "https://storage.googleapis.com/google-code-attachments/#{@repo.name}/issue-#{issue_id}/comment-#{comment_id}/#{filename}"
-
+
text = "[#{filename}](#{link})"
text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/i
text
diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb
index 0cfeaf9d61c..1cb7d16aeb3 100644
--- a/lib/gitlab/google_code_import/project_creator.rb
+++ b/lib/gitlab/google_code_import/project_creator.rb
@@ -23,7 +23,7 @@ module Gitlab
import_url: repo.import_url
).execute
- import_data = project.create_import_data(
+ project.create_import_data(
data: {
"repo" => repo.raw_data,
"user_map" => user_map
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index 856ccc71084..9068d79c95e 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -24,12 +24,12 @@ module Gitlab
match[1]
end
- private
-
def config
Gitlab.config.incoming_email
end
+ private
+
def address_regex
wildcard_address = config.address
return nil unless wildcard_address
diff --git a/lib/gitlab/inline_diff.rb b/lib/gitlab/inline_diff.rb
index 99e7b529ba9..44507bde25d 100644
--- a/lib/gitlab/inline_diff.rb
+++ b/lib/gitlab/inline_diff.rb
@@ -11,48 +11,71 @@ module Gitlab
indexes.each do |index|
first_line = diff_arr[index+1]
second_line = diff_arr[index+2]
- max_length = [first_line.size, second_line.size].max
# Skip inline diff if empty line was replaced with content
next if first_line == "-\n"
- first_the_same_symbols = 0
- (0..max_length + 1).each do |i|
- first_the_same_symbols = i - 1
- if first_line[i] != second_line[i] && i > 0
- break
- end
- end
+ first_token = find_first_token(first_line, second_line)
+ apply_first_token(diff_arr, index, first_token)
+
+ last_token = find_last_token(first_line, second_line, first_token)
+ apply_last_token(diff_arr, index, last_token)
+ end
+
+ diff_arr
+ end
+
+ def apply_first_token(diff_arr, index, first_token)
+ start = first_token + START
+
+ if first_token.empty?
+ # In case if we remove string of spaces in commit
+ diff_arr[index+1].sub!("-", "-" => "-#{START}")
+ diff_arr[index+2].sub!("+", "+" => "+#{START}")
+ else
+ diff_arr[index+1].sub!(first_token, first_token => start)
+ diff_arr[index+2].sub!(first_token, first_token => start)
+ end
+ end
- first_token = first_line[0..first_the_same_symbols][1..-1]
- start = first_token + START
+ def apply_last_token(diff_arr, index, last_token)
+ # This is tricky: escape backslashes so that `sub` doesn't interpret them
+ # as backreferences. Regexp.escape does NOT do the right thing.
+ replace_token = FINISH + last_token.gsub(/\\/, '\&\&')
+ diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, replace_token)
+ diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, replace_token)
+ end
+
+ def find_first_token(first_line, second_line)
+ max_length = [first_line.size, second_line.size].max
+ first_the_same_symbols = 0
+
+ (0..max_length + 1).each do |i|
+ first_the_same_symbols = i - 1
- if first_token.empty?
- # In case if we remove string of spaces in commit
- diff_arr[index+1].sub!("-", "-" => "-#{START}")
- diff_arr[index+2].sub!("+", "+" => "+#{START}")
- else
- diff_arr[index+1].sub!(first_token, first_token => start)
- diff_arr[index+2].sub!(first_token, first_token => start)
+ if first_line[i] != second_line[i] && i > 0
+ break
end
+ end
+
+ first_line[0..first_the_same_symbols][1..-1]
+ end
+
+ def find_last_token(first_line, second_line, first_token)
+ max_length = [first_line.size, second_line.size].max
+ last_the_same_symbols = 0
+
+ (1..max_length + 1).each do |i|
+ last_the_same_symbols = -i
+ shortest_line = second_line.size > first_line.size ? first_line : second_line
- last_the_same_symbols = 0
- (1..max_length + 1).each do |i|
- last_the_same_symbols = -i
- shortest_line = second_line.size > first_line.size ? first_line : second_line
- if ( first_line[-i] != second_line[-i] ) || "#{first_token}#{START}".size == shortest_line[1..-i].size
- break
- end
+ if (first_line[-i] != second_line[-i]) || "#{first_token}#{START}".size == shortest_line[1..-i].size
+ break
end
- last_the_same_symbols += 1
- last_token = first_line[last_the_same_symbols..-1]
- # This is tricky: escape backslashes so that `sub` doesn't interpret them
- # as backreferences. Regexp.escape does NOT do the right thing.
- replace_token = FINISH + last_token.gsub(/\\/, '\&\&')
- diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, replace_token)
- diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, replace_token)
end
- diff_arr
+
+ last_the_same_symbols += 1
+ first_line[last_the_same_symbols..-1]
end
def _indexes_of_changed_lines(diff_arr)
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index cb66fd500fe..4be99dd88c2 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -14,7 +14,7 @@ module Gitlab
# LDAP distinguished name is case-insensitive
identity = ::Identity.
where(provider: provider).
- where('lower(extern_uid) = ?', uid.downcase).last
+ where('lower(extern_uid) = ?', uid.mb_chars.downcase.to_s).last
identity && identity.user
end
end
@@ -35,7 +35,7 @@ module Gitlab
end
def find_by_email
- ::User.find_by(email: auth_hash.email)
+ ::User.find_by(email: auth_hash.email.downcase)
end
def update_user_attributes
diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb
new file mode 100644
index 00000000000..9be9a65671b
--- /dev/null
+++ b/lib/gitlab/lfs/response.rb
@@ -0,0 +1,327 @@
+module Gitlab
+ module Lfs
+ class Response
+
+ def initialize(project, user, request)
+ @origin_project = project
+ @project = storage_project(project)
+ @user = user
+ @env = request.env
+ @request = request
+ end
+
+ def render_download_object_response(oid)
+ render_response_to_download do
+ if check_download_sendfile_header?
+ render_lfs_sendfile(oid)
+ else
+ render_not_found
+ end
+ end
+ end
+
+ def render_batch_operation_response
+ request_body = JSON.parse(@request.body.read)
+ case request_body["operation"]
+ when "download"
+ render_batch_download(request_body)
+ when "upload"
+ render_batch_upload(request_body)
+ else
+ render_not_found
+ end
+ end
+
+ def render_storage_upload_authorize_response(oid, size)
+ render_response_to_push do
+ [
+ 200,
+ { "Content-Type" => "application/json; charset=utf-8" },
+ [JSON.dump({
+ 'StoreLFSPath' => "#{Gitlab.config.lfs.storage_path}/tmp/upload",
+ 'LfsOid' => oid,
+ 'LfsSize' => size
+ })]
+ ]
+ end
+ end
+
+ def render_storage_upload_store_response(oid, size, tmp_file_name)
+ render_response_to_push do
+ render_lfs_upload_ok(oid, size, tmp_file_name)
+ end
+ end
+
+ def render_unsupported_deprecated_api
+ [
+ 501,
+ { "Content-Type" => "application/json; charset=utf-8" },
+ [JSON.dump({
+ 'message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.',
+ 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
+ })]
+ ]
+ end
+
+ private
+
+ def render_not_enabled
+ [
+ 501,
+ {
+ "Content-Type" => "application/json; charset=utf-8",
+ },
+ [JSON.dump({
+ 'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.',
+ 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
+ })]
+ ]
+ end
+
+ def render_unauthorized
+ [
+ 401,
+ {
+ 'Content-Type' => 'text/plain'
+ },
+ ['Unauthorized']
+ ]
+ end
+
+ def render_not_found
+ [
+ 404,
+ {
+ "Content-Type" => "application/vnd.git-lfs+json"
+ },
+ [JSON.dump({
+ 'message' => 'Not found.',
+ 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
+ })]
+ ]
+ end
+
+ def render_forbidden
+ [
+ 403,
+ {
+ "Content-Type" => "application/vnd.git-lfs+json"
+ },
+ [JSON.dump({
+ 'message' => 'Access forbidden. Check your access level.',
+ 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
+ })]
+ ]
+ end
+
+ def render_lfs_sendfile(oid)
+ return render_not_found unless oid.present?
+
+ lfs_object = object_for_download(oid)
+
+ if lfs_object && lfs_object.file.exists?
+ [
+ 200,
+ {
+ # GitLab-workhorse will forward Content-Type header
+ "Content-Type" => "application/octet-stream",
+ "X-Sendfile" => lfs_object.file.path
+ },
+ []
+ ]
+ else
+ render_not_found
+ end
+ end
+
+ def render_batch_upload(body)
+ return render_not_found if body.empty? || body['objects'].nil?
+
+ render_response_to_push do
+ response = build_upload_batch_response(body['objects'])
+ [
+ 200,
+ {
+ "Content-Type" => "application/json; charset=utf-8",
+ "Cache-Control" => "private",
+ },
+ [JSON.dump(response)]
+ ]
+ end
+ end
+
+ def render_batch_download(body)
+ return render_not_found if body.empty? || body['objects'].nil?
+
+ render_response_to_download do
+ response = build_download_batch_response(body['objects'])
+ [
+ 200,
+ {
+ "Content-Type" => "application/json; charset=utf-8",
+ "Cache-Control" => "private",
+ },
+ [JSON.dump(response)]
+ ]
+ end
+ end
+
+ def render_lfs_upload_ok(oid, size, tmp_file)
+ if store_file(oid, size, tmp_file)
+ [
+ 200,
+ {
+ 'Content-Type' => 'text/plain',
+ 'Content-Length' => 0
+ },
+ []
+ ]
+ else
+ [
+ 422,
+ { 'Content-Type' => 'text/plain' },
+ ["Unprocessable entity"]
+ ]
+ end
+ end
+
+ def render_response_to_download
+ return render_not_enabled unless Gitlab.config.lfs.enabled
+
+ unless @project.public?
+ return render_unauthorized unless @user
+ return render_forbidden unless user_can_fetch?
+ end
+
+ yield
+ end
+
+ def render_response_to_push
+ return render_not_enabled unless Gitlab.config.lfs.enabled
+ return render_unauthorized unless @user
+ return render_forbidden unless user_can_push?
+
+ yield
+ end
+
+ def check_download_sendfile_header?
+ @env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile"
+ end
+
+ def user_can_fetch?
+ # Check user access against the project they used to initiate the pull
+ @user.can?(:download_code, @origin_project)
+ end
+
+ def user_can_push?
+ # Check user access against the project they used to initiate the push
+ @user.can?(:push_code, @origin_project)
+ end
+
+ def storage_project(project)
+ if project.forked?
+ project.forked_from_project
+ else
+ project
+ end
+ end
+
+ def store_file(oid, size, tmp_file)
+ tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)
+
+ object = LfsObject.find_or_create_by(oid: oid, size: size)
+ if object.file.exists?
+ success = true
+ else
+ success = move_tmp_file_to_storage(object, tmp_file_path)
+ end
+
+ if success
+ success = link_to_project(object)
+ end
+
+ success
+ ensure
+ # Ensure that the tmp file is removed
+ FileUtils.rm_f(tmp_file_path)
+ end
+
+ def object_for_download(oid)
+ @project.lfs_objects.find_by(oid: oid)
+ end
+
+ def move_tmp_file_to_storage(object, path)
+ File.open(path) do |f|
+ object.file = f
+ end
+
+ object.file.store!
+ object.save
+ end
+
+ def link_to_project(object)
+ if object && !object.projects.exists?(@project.id)
+ object.projects << @project
+ object.save
+ end
+ end
+
+ def select_existing_objects(objects)
+ objects_oids = objects.map { |o| o['oid'] }
+ @project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set
+ end
+
+ def build_upload_batch_response(objects)
+ selected_objects = select_existing_objects(objects)
+
+ upload_hypermedia_links(objects, selected_objects)
+ end
+
+ def build_download_batch_response(objects)
+ selected_objects = select_existing_objects(objects)
+
+ download_hypermedia_links(objects, selected_objects)
+ end
+
+ def download_hypermedia_links(all_objects, existing_objects)
+ all_objects.each do |object|
+ if existing_objects.include?(object['oid'])
+ object['actions'] = {
+ 'download' => {
+ 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}",
+ 'header' => {
+ 'Authorization' => @env['HTTP_AUTHORIZATION']
+ }.compact
+ }
+ }
+ else
+ object['error'] = {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ end
+ end
+
+ { 'objects' => all_objects }
+ end
+
+ def upload_hypermedia_links(all_objects, existing_objects)
+ all_objects.each do |object|
+ # generate actions only for non-existing objects
+ next if existing_objects.include?(object['oid'])
+
+ object['actions'] = {
+ 'upload' => {
+ 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}",
+ 'header' => {
+ 'Authorization' => @env['HTTP_AUTHORIZATION']
+ }.compact
+ }
+ }
+ end
+
+ { 'objects' => all_objects }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb
new file mode 100644
index 00000000000..78d02891102
--- /dev/null
+++ b/lib/gitlab/lfs/router.rb
@@ -0,0 +1,97 @@
+module Gitlab
+ module Lfs
+ class Router
+ def initialize(project, user, request)
+ @project = project
+ @user = user
+ @env = request.env
+ @request = request
+ end
+
+ def try_call
+ return unless @request && @request.path.present?
+
+ case @request.request_method
+ when 'GET'
+ get_response
+ when 'POST'
+ post_response
+ when 'PUT'
+ put_response
+ else
+ nil
+ end
+ end
+
+ private
+
+ def get_response
+ path_match = @request.path.match(/\/(info\/lfs|gitlab-lfs)\/objects\/([0-9a-f]{64})$/)
+ return nil unless path_match
+
+ oid = path_match[2]
+ return nil unless oid
+
+ case path_match[1]
+ when "info/lfs"
+ lfs.render_unsupported_deprecated_api
+ when "gitlab-lfs"
+ lfs.render_download_object_response(oid)
+ else
+ nil
+ end
+ end
+
+ def post_response
+ post_path = @request.path.match(/\/info\/lfs\/objects(\/batch)?$/)
+ return nil unless post_path
+
+ # Check for Batch API
+ if post_path[0].ends_with?("/info/lfs/objects/batch")
+ lfs.render_batch_operation_response
+ elsif post_path[0].ends_with?("/info/lfs/objects")
+ lfs.render_unsupported_deprecated_api
+ else
+ nil
+ end
+ end
+
+ def put_response
+ object_match = @request.path.match(/\/gitlab-lfs\/objects\/([0-9a-f]{64})\/([0-9]+)(|\/authorize){1}$/)
+ return nil if object_match.nil?
+
+ oid = object_match[1]
+ size = object_match[2].try(:to_i)
+ return nil if oid.nil? || size.nil?
+
+ # GitLab-workhorse requests
+ # 1. Try to authorize the request
+ # 2. send a request with a header containing the name of the temporary file
+ if object_match[3] && object_match[3] == '/authorize'
+ lfs.render_storage_upload_authorize_response(oid, size)
+ else
+ tmp_file_name = sanitize_tmp_filename(@request.env['HTTP_X_GITLAB_LFS_TMP'])
+ return nil unless tmp_file_name
+
+ lfs.render_storage_upload_store_response(oid, size, tmp_file_name)
+ end
+ end
+
+ def lfs
+ return unless @project
+
+ Gitlab::Lfs::Response.new(@project, @user, @request)
+ end
+
+ def sanitize_tmp_filename(name)
+ if name.present?
+ name.gsub!(/^.*(\\|\/)/, '')
+ name = name.match(/[0-9a-f]{73}/)
+ name[0] if name
+ else
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb
index ae5f2544691..886a09f52af 100644
--- a/lib/gitlab/markdown.rb
+++ b/lib/gitlab/markdown.rb
@@ -7,6 +7,14 @@ module Gitlab
module Markdown
# Convert a Markdown String into an HTML-safe String of HTML
#
+ # Note that while the returned HTML will have been sanitized of dangerous
+ # HTML, it may post a risk of information leakage if it's not also passed
+ # through `post_process`.
+ #
+ # Also note that the returned String is always HTML, not XHTML. Views
+ # requiring XHTML, such as Atom feeds, need to call `post_process` on the
+ # result, providing the appropriate `pipeline` option.
+ #
# markdown - Markdown String
# context - Hash of context options passed to our HTML Pipeline
#
@@ -31,6 +39,33 @@ module Gitlab
renderer.render(markdown)
end
+ # Perform post-processing on an HTML String
+ #
+ # This method is used to perform state-dependent changes to a String of
+ # HTML, such as removing references that the current user doesn't have
+ # permission to make (`RedactorFilter`).
+ #
+ # html - String to process
+ # options - Hash of options to customize output
+ # :pipeline - Symbol pipeline type
+ # :project - Project
+ # :user - User object
+ #
+ # Returns an HTML-safe String
+ def self.post_process(html, options)
+ context = {
+ project: options[:project],
+ current_user: options[:user]
+ }
+ doc = post_processor.to_document(html, context)
+
+ if options[:pipeline] == :atom
+ doc.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
+ else
+ doc.to_html
+ end.html_safe
+ end
+
# Provide autoload paths for filters to prevent a circular dependency error
autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter'
autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter'
@@ -41,6 +76,7 @@ module Gitlab
autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter'
autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter'
autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter'
+ autoload :RedactorFilter, 'gitlab/markdown/redactor_filter'
autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter'
autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter'
autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter'
@@ -48,27 +84,22 @@ module Gitlab
autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter'
autoload :TaskListFilter, 'gitlab/markdown/task_list_filter'
autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter'
+ autoload :UploadLinkFilter, 'gitlab/markdown/upload_link_filter'
- # Public: Parse the provided text with GitLab-Flavored Markdown
+ # Public: Parse the provided HTML with GitLab-Flavored Markdown
+ #
+ # html - HTML String
+ # options - A Hash of options used to customize output (default: {})
+ # :no_header_anchors - Disable header anchors in TableOfContentsFilter
+ # :path - Current path String
+ # :pipeline - Symbol pipeline type
+ # :project - Current Project object
+ # :project_wiki - Current ProjectWiki object
+ # :ref - Current ref String
#
- # text - the source text
- # options - A Hash of options used to customize output (default: {}):
- # :xhtml - output XHTML instead of HTML
- # :reference_only_path - Use relative path for reference links
- def self.gfm(text, options = {})
- return text if text.nil?
-
- # Duplicate the string so we don't alter the original, then call to_str
- # to cast it back to a String instead of a SafeBuffer. This is required
- # for gsub calls to work as we need them to.
- text = text.dup.to_str
-
- options.reverse_merge!(
- xhtml: false,
- reference_only_path: true,
- project: options[:project],
- current_user: options[:current_user]
- )
+ # Returns an HTML-safe String
+ def self.gfm(html, options = {})
+ return '' unless html.present?
@pipeline ||= HTML::Pipeline.new(filters)
@@ -77,41 +108,36 @@ module Gitlab
pipeline: options[:pipeline],
# EmojiFilter
- asset_root: Gitlab.config.gitlab.base_url,
asset_host: Gitlab::Application.config.asset_host,
-
- # TableOfContentsFilter
- no_header_anchors: options[:no_header_anchors],
+ asset_root: Gitlab.config.gitlab.base_url,
# ReferenceFilter
- current_user: options[:current_user],
- only_path: options[:reference_only_path],
- project: options[:project],
+ only_path: only_path_pipeline?(options[:pipeline]),
+ project: options[:project],
# RelativeLinkFilter
+ project_wiki: options[:project_wiki],
ref: options[:ref],
requested_path: options[:path],
- project_wiki: options[:project_wiki]
- }
-
- result = @pipeline.call(text, context)
-
- save_options = 0
- if options[:xhtml]
- save_options |= Nokogiri::XML::Node::SaveOptions::AS_XHTML
- end
- text = result[:output].to_html(save_with: save_options)
+ # TableOfContentsFilter
+ no_header_anchors: options[:no_header_anchors]
+ }
- text.html_safe
+ @pipeline.to_html(html, context).html_safe
end
private
- def self.renderer
- @markdown ||= begin
- renderer = Redcarpet::Render::HTML.new
- Redcarpet::Markdown.new(renderer, redcarpet_options)
+ # Check if a pipeline enables the `only_path` context option
+ #
+ # Returns Boolean
+ def self.only_path_pipeline?(pipeline)
+ case pipeline
+ when :atom, :email
+ false
+ else
+ true
end
end
@@ -129,6 +155,17 @@ module Gitlab
}.freeze
end
+ def self.renderer
+ @markdown ||= begin
+ renderer = Redcarpet::Render::HTML.new
+ Redcarpet::Markdown.new(renderer, redcarpet_options)
+ end
+ end
+
+ def self.post_processor
+ @post_processor ||= HTML::Pipeline.new([Gitlab::Markdown::RedactorFilter])
+ end
+
# Filters used in our pipeline
#
# SanitizationFilter should come first so that all generated reference HTML
@@ -140,7 +177,7 @@ module Gitlab
Gitlab::Markdown::SyntaxHighlightFilter,
Gitlab::Markdown::SanitizationFilter,
- Gitlab::Markdown::RelativeLinkFilter,
+ Gitlab::Markdown::UploadLinkFilter,
Gitlab::Markdown::EmojiFilter,
Gitlab::Markdown::TableOfContentsFilter,
Gitlab::Markdown::AutolinkFilter,
@@ -155,6 +192,8 @@ module Gitlab
Gitlab::Markdown::CommitReferenceFilter,
Gitlab::Markdown::LabelReferenceFilter,
+ Gitlab::Markdown::RelativeLinkFilter,
+
Gitlab::Markdown::TaskListFilter
]
end
diff --git a/lib/gitlab/markdown/abstract_reference_filter.rb b/lib/gitlab/markdown/abstract_reference_filter.rb
new file mode 100644
index 00000000000..9488e980c08
--- /dev/null
+++ b/lib/gitlab/markdown/abstract_reference_filter.rb
@@ -0,0 +1,145 @@
+require 'gitlab/markdown'
+
+module Gitlab
+ module Markdown
+ # Issues, Merge Requests, Snippets, Commits and Commit Ranges share
+ # similar functionality in reference filtering.
+ class AbstractReferenceFilter < ReferenceFilter
+ include CrossProjectReference
+
+ def self.object_class
+ # Implement in child class
+ # Example: MergeRequest
+ end
+
+ def self.object_name
+ object_class.name.underscore
+ end
+
+ def self.object_sym
+ object_name.to_sym
+ end
+
+ def self.data_reference
+ "data-#{object_name.dasherize}"
+ end
+
+ # Public: Find references in text (like `!123` for merge requests)
+ #
+ # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
+ # object = find_object(project_ref, id)
+ # "<a href=...>#{object.to_reference}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer referenced object ID, an optional String
+ # of the external project reference, and all of the matchdata.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text, pattern = object_class.reference_pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[object_sym].to_i, $~[:project], $~
+ end
+ end
+
+ def self.referenced_by(node)
+ { object_sym => LazyReference.new(object_class, node.attr(data_reference)) }
+ end
+
+ delegate :object_class, :object_sym, :references_in, to: :class
+
+ def find_object(project, id)
+ # Implement in child class
+ # Example: project.merge_requests.find
+ end
+
+ def url_for_object(object, project)
+ # Implement in child class
+ # Example: project_merge_request_url
+ end
+
+ def call
+ # `#123`
+ replace_text_nodes_matching(object_class.reference_pattern) do |content|
+ object_link_filter(content, object_class.reference_pattern)
+ end
+
+ # `[Issue](#123)`, which is turned into
+ # `<a href="#123">Issue</a>`
+ replace_link_nodes_with_href(object_class.reference_pattern) do |link, text|
+ object_link_filter(link, object_class.reference_pattern, link_text: text)
+ end
+
+ # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into
+ # `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>`
+ replace_link_nodes_with_text(object_class.link_reference_pattern) do |text|
+ object_link_filter(text, object_class.link_reference_pattern)
+ end
+
+ # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into
+ # `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>`
+ replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text|
+ object_link_filter(link, object_class.link_reference_pattern, link_text: text)
+ end
+ end
+
+ # Replace references (like `!123` for merge requests) in text with links
+ # to the referenced object's details page.
+ #
+ # text - String text to replace references in.
+ # pattern - Reference pattern to match against.
+ # link_text - Original content of the link being replaced.
+ #
+ # Returns a String with references replaced with links. All links
+ # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
+ def object_link_filter(text, pattern, link_text: nil)
+ references_in(text, pattern) do |match, id, project_ref, matches|
+ project = project_from_ref(project_ref)
+
+ if project && object = find_object(project, id)
+ title = escape_once(object_link_title(object))
+ klass = reference_class(object_sym)
+
+ data = data_attribute(
+ original: link_text || match,
+ project: project.id,
+ object_sym => object.id
+ )
+
+ url = matches[:url] if matches.names.include?("url")
+ url ||= url_for_object(object, project)
+
+ text = link_text
+ unless text
+ text = object.reference_link_text(context[:project])
+
+ extras = object_link_text_extras(object, matches)
+ text += " (#{extras.join(", ")})" if extras.any?
+ end
+
+ %(<a href="#{url}" #{data}
+ title="#{title}"
+ class="#{klass}">#{text}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def object_link_text_extras(object, matches)
+ extras = []
+
+ if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/
+ extras << "comment #{$1}"
+ end
+
+ extras
+ end
+
+ def object_link_title(object)
+ "#{object_class.name.titleize}: #{object.title}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb
index bb496135d92..36b3258ef76 100644
--- a/lib/gitlab/markdown/commit_range_reference_filter.rb
+++ b/lib/gitlab/markdown/commit_range_reference_filter.rb
@@ -5,78 +5,54 @@ module Gitlab
# HTML filter that replaces commit range references with links.
#
# This filter supports cross-project references.
- class CommitRangeReferenceFilter < ReferenceFilter
- include CrossProjectReference
+ class CommitRangeReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ CommitRange
+ end
- # Public: Find commit range references in text
- #
- # CommitRangeReferenceFilter.references_in(text) do |match, commit_range, project_ref|
- # "<a href=...>#{commit_range}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, the String commit range, and an optional String
- # of the external project reference.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(CommitRange.reference_pattern) do |match|
- yield match, $~[:commit_range], $~[:project]
+ def self.references_in(text, pattern = CommitRange.reference_pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[:commit_range], $~[:project], $~
end
end
- def initialize(*args)
- super
+ def self.referenced_by(node)
+ project = Project.find(node.attr("data-project")) rescue nil
+ return unless project
- @commit_map = {}
- end
+ id = node.attr("data-commit-range")
+ range = find_object(project, id)
- def call
- replace_text_nodes_matching(CommitRange.reference_pattern) do |content|
- commit_range_link_filter(content)
- end
- end
+ return unless range
- # Replace commit range references in text with links to compare the commit
- # ranges.
- #
- # text - String text to replace references in.
- #
- # Returns a String with commit range references replaced with links. All
- # links have `gfm` and `gfm-commit_range` class names attached for
- # styling.
- def commit_range_link_filter(text)
- self.class.references_in(text) do |match, id, project_ref|
- project = self.project_from_ref(project_ref)
-
- range = CommitRange.new(id, project)
+ { commit_range: range }
+ end
- if range.valid_commits?
- push_result(:commit_range, range)
+ def initialize(*args)
+ super
- url = url_for_commit_range(project, range)
+ @commit_map = {}
+ end
- title = range.reference_title
- klass = reference_class(:commit_range)
- data = data_attribute(project.id)
+ def self.find_object(project, id)
+ range = CommitRange.new(id, project)
- project_ref += '@' if project_ref
+ range.valid_commits? ? range : nil
+ end
- %(<a href="#{url}" #{data}
- title="#{title}"
- class="#{klass}">#{project_ref}#{range}</a>)
- else
- match
- end
- end
+ def find_object(*args)
+ self.class.find_object(*args)
end
- def url_for_commit_range(project, range)
+ def url_for_object(range, project)
h = Gitlab::Application.routes.url_helpers
h.namespace_project_compare_url(project.namespace, project,
range.to_param.merge(only_path: context[:only_path]))
end
+
+ def object_link_title(range)
+ range.reference_title
+ end
end
end
end
diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb
index fcbb2e936a5..b4036578e60 100644
--- a/lib/gitlab/markdown/commit_reference_filter.rb
+++ b/lib/gitlab/markdown/commit_reference_filter.rb
@@ -5,74 +5,48 @@ module Gitlab
# HTML filter that replaces commit references with links.
#
# This filter supports cross-project references.
- class CommitReferenceFilter < ReferenceFilter
- include CrossProjectReference
-
- # Public: Find commit references in text
- #
- # CommitReferenceFilter.references_in(text) do |match, commit, project_ref|
- # "<a href=...>#{commit}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, the String commit identifier, and an optional
- # String of the external project reference.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(Commit.reference_pattern) do |match|
- yield match, $~[:commit], $~[:project]
- end
+ class CommitReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ Commit
end
- def call
- replace_text_nodes_matching(Commit.reference_pattern) do |content|
- commit_link_filter(content)
+ def self.references_in(text, pattern = Commit.reference_pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[:commit], $~[:project], $~
end
end
- # Replace commit references in text with links to the commit specified.
- #
- # text - String text to replace references in.
- #
- # Returns a String with commit references replaced with links. All links
- # have `gfm` and `gfm-commit` class names attached for styling.
- def commit_link_filter(text)
- self.class.references_in(text) do |match, commit_ref, project_ref|
- project = self.project_from_ref(project_ref)
-
- if commit = commit_from_ref(project, commit_ref)
- push_result(:commit, commit)
+ def self.referenced_by(node)
+ project = Project.find(node.attr("data-project")) rescue nil
+ return unless project
- url = url_for_commit(project, commit)
+ id = node.attr("data-commit")
+ commit = find_object(project, id)
- title = escape_once(commit.link_title)
- klass = reference_class(:commit)
- data = data_attribute(project.id)
+ return unless commit
- project_ref += '@' if project_ref
-
- %(<a href="#{url}" #{data}
- title="#{title}"
- class="#{klass}">#{project_ref}#{commit.short_id}</a>)
- else
- match
- end
- end
+ { commit: commit }
end
- def commit_from_ref(project, commit_ref)
+ def self.find_object(project, id)
if project && project.valid_repo?
- project.commit(commit_ref)
+ project.commit(id)
end
end
- def url_for_commit(project, commit)
+ def find_object(*args)
+ self.class.find_object(*args)
+ end
+
+ def url_for_object(commit, project)
h = Gitlab::Application.routes.url_helpers
h.namespace_project_commit_url(project.namespace, project, commit,
only_path: context[:only_path])
end
+
+ def object_link_title(commit)
+ commit.link_title
+ end
end
end
end
diff --git a/lib/gitlab/markdown/cross_project_reference.rb b/lib/gitlab/markdown/cross_project_reference.rb
index 855748fdccc..6ab04a584b0 100644
--- a/lib/gitlab/markdown/cross_project_reference.rb
+++ b/lib/gitlab/markdown/cross_project_reference.rb
@@ -13,18 +13,11 @@ module Gitlab
#
# ref - String reference.
#
- # Returns a Project, or nil if the reference can't be accessed
+ # Returns a Project, or nil if the reference can't be found
def project_from_ref(ref)
return context[:project] unless ref
- other = Project.find_with_namespace(ref)
- return nil unless other && user_can_reference_project?(other)
-
- other
- end
-
- def user_can_reference_project?(project, user = context[:current_user])
- Ability.abilities.allowed?(user, :read_project, project)
+ Project.find_with_namespace(ref)
end
end
end
diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/external_issue_reference_filter.rb
index f7c43e1ca89..14bdf5521fc 100644
--- a/lib/gitlab/markdown/external_issue_reference_filter.rb
+++ b/lib/gitlab/markdown/external_issue_reference_filter.rb
@@ -30,6 +30,10 @@ module Gitlab
replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content|
issue_link_filter(content)
end
+
+ replace_link_nodes_with_href(ExternalIssue.reference_pattern) do |link, text|
+ issue_link_filter(link, link_text: text)
+ end
end
# Replace `JIRA-123` issue references in text with links to the referenced
@@ -39,7 +43,7 @@ module Gitlab
#
# Returns a String with `JIRA-123` references replaced with links. All
# links have `gfm` and `gfm-issue` class names attached for styling.
- def issue_link_filter(text)
+ def issue_link_filter(text, link_text: nil)
project = context[:project]
self.class.references_in(text) do |match, issue|
@@ -47,10 +51,13 @@ module Gitlab
title = escape_once("Issue in #{project.external_issue_tracker.title}")
klass = reference_class(:issue)
+ data = data_attribute(project: project.id)
+
+ text = link_text || match
- %(<a href="#{url}"
+ %(<a href="#{url}" #{data}
title="#{title}"
- class="#{klass}">#{match}</a>)
+ class="#{klass}">#{text}</a>)
end
end
diff --git a/lib/gitlab/markdown/external_link_filter.rb b/lib/gitlab/markdown/external_link_filter.rb
index 29e51b6ade6..e09dfcb83c8 100644
--- a/lib/gitlab/markdown/external_link_filter.rb
+++ b/lib/gitlab/markdown/external_link_filter.rb
@@ -8,9 +8,9 @@ module Gitlab
class ExternalLinkFilter < HTML::Pipeline::Filter
def call
doc.search('a').each do |node|
- next unless node.has_attribute?('href')
+ link = node.attr('href')
- link = node.attribute('href').value
+ next unless link
# Skip non-HTTP(S) links
next unless link.start_with?('http')
diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb
index 01320f80796..1ed69e2f431 100644
--- a/lib/gitlab/markdown/issue_reference_filter.rb
+++ b/lib/gitlab/markdown/issue_reference_filter.rb
@@ -6,64 +6,17 @@ module Gitlab
# issues that do not exist are ignored.
#
# This filter supports cross-project references.
- class IssueReferenceFilter < ReferenceFilter
- include CrossProjectReference
-
- # Public: Find `#123` issue references in text
- #
- # IssueReferenceFilter.references_in(text) do |match, issue, project_ref|
- # "<a href=...>##{issue}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, the Integer issue ID, and an optional String of
- # the external project reference.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(Issue.reference_pattern) do |match|
- yield match, $~[:issue].to_i, $~[:project]
- end
- end
-
- def call
- replace_text_nodes_matching(Issue.reference_pattern) do |content|
- issue_link_filter(content)
- end
+ class IssueReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ Issue
end
- # Replace `#123` issue references in text with links to the referenced
- # issue's details page.
- #
- # text - String text to replace references in.
- #
- # Returns a String with `#123` references replaced with links. All links
- # have `gfm` and `gfm-issue` class names attached for styling.
- def issue_link_filter(text)
- self.class.references_in(text) do |match, id, project_ref|
- project = self.project_from_ref(project_ref)
-
- if project && issue = project.get_issue(id)
- push_result(:issue, issue)
-
- url = url_for_issue(id, project, only_path: context[:only_path])
-
- title = escape_once("Issue: #{issue.title}")
- klass = reference_class(:issue)
- data = data_attribute(project.id)
-
- %(<a href="#{url}" #{data}
- title="#{title}"
- class="#{klass}">#{match}</a>)
- else
- match
- end
- end
+ def find_object(project, id)
+ project.get_issue(id)
end
- def url_for_issue(*args)
- IssuesHelper.url_for_issue(*args)
+ def url_for_object(issue, project)
+ IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path])
end
end
end
diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb
index 1e5cb12071e..a2026eecaeb 100644
--- a/lib/gitlab/markdown/label_reference_filter.rb
+++ b/lib/gitlab/markdown/label_reference_filter.rb
@@ -22,10 +22,18 @@ module Gitlab
end
end
+ def self.referenced_by(node)
+ { label: LazyReference.new(Label, node.attr("data-label")) }
+ end
+
def call
replace_text_nodes_matching(Label.reference_pattern) do |content|
label_link_filter(content)
end
+
+ replace_link_nodes_with_href(Label.reference_pattern) do |link, text|
+ label_link_filter(link, link_text: text)
+ end
end
# Replace label references in text with links to the label specified.
@@ -34,21 +42,25 @@ module Gitlab
#
# Returns a String with label references replaced with links. All links
# have `gfm` and `gfm-label` class names attached for styling.
- def label_link_filter(text)
+ def label_link_filter(text, link_text: nil)
project = context[:project]
self.class.references_in(text) do |match, id, name|
params = label_params(id, name)
if label = project.labels.find_by(params)
- push_result(:label, label)
-
url = url_for_label(project, label)
klass = reference_class(:label)
- data = data_attribute(project.id)
+ data = data_attribute(
+ original: link_text || match,
+ project: project.id,
+ label: label.id
+ )
+
+ text = link_text || render_colored_label(label)
%(<a href="#{url}" #{data}
- class="#{klass}">#{render_colored_label(label)}</a>)
+ class="#{klass}">#{text}</a>)
else
match
end
@@ -57,9 +69,8 @@ module Gitlab
def url_for_label(project, label)
h = Gitlab::Application.routes.url_helpers
- h.namespace_project_issues_path(project.namespace, project,
- label_name: label.name,
- only_path: context[:only_path])
+ h.namespace_project_issues_url( project.namespace, project, label_name: label.name,
+ only_path: context[:only_path])
end
def render_colored_label(label)
diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb
index ecbd263d0e0..de71fc76a9b 100644
--- a/lib/gitlab/markdown/merge_request_reference_filter.rb
+++ b/lib/gitlab/markdown/merge_request_reference_filter.rb
@@ -6,66 +6,29 @@ module Gitlab
# to merge requests that do not exist are ignored.
#
# This filter supports cross-project references.
- class MergeRequestReferenceFilter < ReferenceFilter
- include CrossProjectReference
-
- # Public: Find `!123` merge request references in text
- #
- # MergeRequestReferenceFilter.references_in(text) do |match, merge_request, project_ref|
- # "<a href=...>##{merge_request}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, the Integer merge request ID, and an optional
- # String of the external project reference.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(MergeRequest.reference_pattern) do |match|
- yield match, $~[:merge_request].to_i, $~[:project]
- end
+ class MergeRequestReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ MergeRequest
end
- def call
- replace_text_nodes_matching(MergeRequest.reference_pattern) do |content|
- merge_request_link_filter(content)
- end
+ def find_object(project, id)
+ project.merge_requests.find_by(iid: id)
end
- # Replace `!123` merge request references in text with links to the
- # referenced merge request's details page.
- #
- # text - String text to replace references in.
- #
- # Returns a String with `!123` references replaced with links. All links
- # have `gfm` and `gfm-merge_request` class names attached for styling.
- def merge_request_link_filter(text)
- self.class.references_in(text) do |match, id, project_ref|
- project = self.project_from_ref(project_ref)
-
- if project && merge_request = project.merge_requests.find_by(iid: id)
- push_result(:merge_request, merge_request)
-
- title = escape_once("Merge Request: #{merge_request.title}")
- klass = reference_class(:merge_request)
- data = data_attribute(project.id)
+ def url_for_object(mr, project)
+ h = Gitlab::Application.routes.url_helpers
+ h.namespace_project_merge_request_url(project.namespace, project, mr,
+ only_path: context[:only_path])
+ end
- url = url_for_merge_request(merge_request, project)
+ def object_link_text_extras(object, matches)
+ extras = super
- %(<a href="#{url}" #{data}
- title="#{title}"
- class="#{klass}">#{match}</a>)
- else
- match
- end
+ if matches.names.include?("path") && matches[:path] && matches[:path] == '/diffs'
+ extras.unshift "diffs"
end
- end
- def url_for_merge_request(mr, project)
- h = Gitlab::Application.routes.url_helpers
- h.namespace_project_merge_request_url(project.namespace, project, mr,
- only_path: context[:only_path])
+ extras
end
end
end
diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/redactor_filter.rb
new file mode 100644
index 00000000000..bea714a01e7
--- /dev/null
+++ b/lib/gitlab/markdown/redactor_filter.rb
@@ -0,0 +1,43 @@
+require 'gitlab/markdown'
+require 'html/pipeline/filter'
+
+module Gitlab
+ module Markdown
+ # HTML filter that removes references to records that the current user does
+ # not have permission to view.
+ #
+ # Expected to be run in its own post-processing pipeline.
+ #
+ class RedactorFilter < HTML::Pipeline::Filter
+ def call
+ doc.css('a.gfm').each do |node|
+ unless user_can_reference?(node)
+ # The reference should be replaced by the original text,
+ # which is not always the same as the rendered text.
+ text = node.attr('data-original') || node.text
+ node.replace(text)
+ end
+ end
+
+ doc
+ end
+
+ private
+
+ def user_can_reference?(node)
+ if node.has_attribute?('data-reference-filter')
+ reference_type = node.attr('data-reference-filter')
+ reference_filter = reference_type.constantize
+
+ reference_filter.user_can_reference?(current_user, node, context)
+ else
+ true
+ end
+ end
+
+ def current_user
+ context[:current_user]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb
index 9b293c957d6..b6d93e05ec7 100644
--- a/lib/gitlab/markdown/reference_filter.rb
+++ b/lib/gitlab/markdown/reference_filter.rb
@@ -11,30 +11,57 @@ module Gitlab
# Context options:
# :project (required) - Current project, ignored if reference is cross-project.
# :only_path - Generate path-only links.
- #
- # Results:
- # :references - A Hash of references that were found and replaced.
class ReferenceFilter < HTML::Pipeline::Filter
- def initialize(*args)
- super
+ LazyReference = Struct.new(:klass, :ids) do
+ def self.load(refs)
+ lazy_references, values = refs.partition { |ref| ref.is_a?(self) }
+
+ lazy_values = lazy_references.group_by(&:klass).flat_map do |klass, refs|
+ ids = refs.flat_map(&:ids)
+ klass.where(id: ids)
+ end
+
+ values + lazy_values
+ end
+
+ def load
+ self.klass.where(id: self.ids)
+ end
+ end
+
+ def self.user_can_reference?(user, node, context)
+ if node.has_attribute?('data-project')
+ project_id = node.attr('data-project').to_i
+ return true if project_id == context[:project].try(:id)
- result[:references] = Hash.new { |hash, type| hash[type] = [] }
+ project = Project.find(project_id) rescue nil
+ Ability.abilities.allowed?(user, :read_project, project)
+ else
+ true
+ end
+ end
+
+ def self.referenced_by(node)
+ raise NotImplementedError, "#{self} does not implement #{__method__}"
end
# Returns a data attribute String to attach to a reference link
#
- # id - Object ID
- # type - Object type (default: :project)
+ # attributes - Hash, where the key becomes the data attribute name and the
+ # value is the data attribute value
#
# Examples:
#
- # data_attribute(1) # => "data-project-id=\"1\""
- # data_attribute(2, :user) # => "data-user-id=\"2\""
- # data_attribute(3, :group) # => "data-group-id=\"3\""
+ # data_attribute(project: 1, issue: 2)
+ # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
+ #
+ # data_attribute(project: 3, merge_request: 4)
+ # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
#
# Returns a String
- def data_attribute(id, type = :project)
- %Q(data-#{type}-id="#{id}")
+ def data_attribute(attributes = {})
+ attributes[:reference_filter] = self.class.name
+ attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ")
end
def escape_once(html)
@@ -59,16 +86,6 @@ module Gitlab
context[:project]
end
- # Add a reference to the pipeline's result Hash
- #
- # type - Singular Symbol reference type (e.g., :issue, :user, etc.)
- # values - One or more Objects to add
- def push_result(type, *values)
- return if values.empty?
-
- result[:references][type].push(*values)
- end
-
def reference_class(type)
"gfm gfm-#{type}"
end
@@ -85,15 +102,15 @@ module Gitlab
# Yields the current node's String contents. The result of the block will
# replace the node's existing content and update the current document.
#
- # Returns the updated Nokogiri::XML::Document object.
+ # Returns the updated Nokogiri::HTML::DocumentFragment object.
def replace_text_nodes_matching(pattern)
return doc if project.nil?
search_text_nodes(doc).each do |node|
- content = node.to_html
-
- next unless content.match(pattern)
next if ignored_ancestry?(node)
+ next unless node.text =~ pattern
+
+ content = node.to_html
html = yield content
@@ -105,6 +122,80 @@ module Gitlab
doc
end
+ # Iterate through the document's link nodes, yielding the current node's
+ # content if:
+ #
+ # * The `project` context value is present AND
+ # * The node's content matches `pattern`
+ #
+ # pattern - Regex pattern against which to match the node's content
+ #
+ # Yields the current node's String contents. The result of the block will
+ # replace the node and update the current document.
+ #
+ # Returns the updated Nokogiri::HTML::DocumentFragment object.
+ def replace_link_nodes_with_text(pattern)
+ return doc if project.nil?
+
+ doc.search('a').each do |node|
+ klass = node.attr('class')
+ next if klass && klass.include?('gfm')
+
+ link = node.attr('href')
+ text = node.text
+
+ next unless link && text
+
+ link = URI.decode(link)
+ # Ignore ending punctionation like periods or commas
+ next unless link == text && text =~ /\A#{pattern}/
+
+ html = yield text
+
+ next if html == text
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ # Iterate through the document's link nodes, yielding the current node's
+ # content if:
+ #
+ # * The `project` context value is present AND
+ # * The node's HREF matches `pattern`
+ #
+ # pattern - Regex pattern against which to match the node's HREF
+ #
+ # Yields the current node's String HREF and String content.
+ # The result of the block will replace the node and update the current document.
+ #
+ # Returns the updated Nokogiri::HTML::DocumentFragment object.
+ def replace_link_nodes_with_href(pattern)
+ return doc if project.nil?
+
+ doc.search('a').each do |node|
+ klass = node.attr('class')
+ next if klass && klass.include?('gfm')
+
+ link = node.attr('href')
+ text = node.text
+
+ next unless link && text
+ link = URI.decode(link)
+ next unless link && link =~ /\A#{pattern}\z/
+
+ html = yield link, text
+
+ next if html == link
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
# Ensure that a :project key exists in context
#
# Note that while the key might exist, its value could be nil!
diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/reference_gatherer_filter.rb
new file mode 100644
index 00000000000..00f983675e6
--- /dev/null
+++ b/lib/gitlab/markdown/reference_gatherer_filter.rb
@@ -0,0 +1,63 @@
+require 'gitlab/markdown'
+require 'html/pipeline/filter'
+
+module Gitlab
+ module Markdown
+ # HTML filter that gathers all referenced records that the current user has
+ # permission to view.
+ #
+ # Expected to be run in its own post-processing pipeline.
+ #
+ class ReferenceGathererFilter < HTML::Pipeline::Filter
+ def initialize(*)
+ super
+
+ result[:references] ||= Hash.new { |hash, type| hash[type] = [] }
+ end
+
+ def call
+ doc.css('a.gfm').each do |node|
+ gather_references(node)
+ end
+
+ load_lazy_references unless context[:load_lazy_references] == false
+
+ doc
+ end
+
+ private
+
+ def gather_references(node)
+ return unless node.has_attribute?('data-reference-filter')
+
+ reference_type = node.attr('data-reference-filter')
+ reference_filter = reference_type.constantize
+
+ return if context[:reference_filter] && reference_filter != context[:reference_filter]
+
+ return unless reference_filter.user_can_reference?(current_user, node, context)
+
+ references = reference_filter.referenced_by(node)
+ return unless references
+
+ references.each do |type, values|
+ Array.wrap(values).each do |value|
+ result[:references][type] << value
+ end
+ end
+ end
+
+ # Will load all references of one type using one query.
+ def load_lazy_references
+ refs = result[:references]
+ refs.each do |type, values|
+ refs[type] = ReferenceFilter::LazyReference.load(values)
+ end
+ end
+
+ def current_user
+ context[:current_user]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/relative_link_filter.rb
index 6ee3d1ce039..692c51fd324 100644
--- a/lib/gitlab/markdown/relative_link_filter.rb
+++ b/lib/gitlab/markdown/relative_link_filter.rb
@@ -17,6 +17,9 @@ module Gitlab
return doc unless linkable_files?
doc.search('a').each do |el|
+ klass = el.attr('class')
+ next if klass && klass.include?('gfm')
+
process_link_attr el.attribute('href')
end
@@ -51,7 +54,7 @@ module Gitlab
relative_url_root,
context[:project].path_with_namespace,
path_type(file_path),
- ref || 'master', # assume that if no ref exists we can point to master
+ ref || context[:project].default_branch, # if no ref exists, point to the default branch
file_path
].compact.join('/').squeeze('/').chomp('/')
diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/sanitization_filter.rb
index e368de7d848..ffb9dc33b64 100644
--- a/lib/gitlab/markdown/sanitization_filter.rb
+++ b/lib/gitlab/markdown/sanitization_filter.rb
@@ -48,6 +48,12 @@ module Gitlab
# Allow span elements
whitelist[:elements].push('span')
+ # Allow any protocol in `a` elements...
+ whitelist[:protocols].delete('a')
+
+ # ...but then remove links with the `javascript` protocol
+ whitelist[:transformers].push(remove_javascript_links)
+
# Remove `rel` attribute from `a` elements
whitelist[:transformers].push(remove_rel)
@@ -57,6 +63,19 @@ module Gitlab
whitelist
end
+ def remove_javascript_links
+ lambda do |env|
+ node = env[:node]
+
+ return unless node.name == 'a'
+ return unless node.has_attribute?('href')
+
+ if node['href'].start_with?('javascript', ':javascript')
+ node.remove_attribute('href')
+ end
+ end
+ end
+
def remove_rel
lambda do |env|
if env[:node_name] == 'a'
diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb
index e2cf89cb1d8..f7bd07c2a34 100644
--- a/lib/gitlab/markdown/snippet_reference_filter.rb
+++ b/lib/gitlab/markdown/snippet_reference_filter.rb
@@ -6,63 +6,16 @@ module Gitlab
# snippets that do not exist are ignored.
#
# This filter supports cross-project references.
- class SnippetReferenceFilter < ReferenceFilter
- include CrossProjectReference
-
- # Public: Find `$123` snippet references in text
- #
- # SnippetReferenceFilter.references_in(text) do |match, snippet|
- # "<a href=...>$#{snippet}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, the Integer snippet ID, and an optional String
- # of the external project reference.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(Snippet.reference_pattern) do |match|
- yield match, $~[:snippet].to_i, $~[:project]
- end
- end
-
- def call
- replace_text_nodes_matching(Snippet.reference_pattern) do |content|
- snippet_link_filter(content)
- end
+ class SnippetReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ Snippet
end
- # Replace `$123` snippet references in text with links to the referenced
- # snippets's details page.
- #
- # text - String text to replace references in.
- #
- # Returns a String with `$123` references replaced with links. All links
- # have `gfm` and `gfm-snippet` class names attached for styling.
- def snippet_link_filter(text)
- self.class.references_in(text) do |match, id, project_ref|
- project = self.project_from_ref(project_ref)
-
- if project && snippet = project.snippets.find_by(id: id)
- push_result(:snippet, snippet)
-
- title = escape_once("Snippet: #{snippet.title}")
- klass = reference_class(:snippet)
- data = data_attribute(project.id)
-
- url = url_for_snippet(snippet, project)
-
- %(<a href="#{url}" #{data}
- title="#{title}"
- class="#{klass}">#{match}</a>)
- else
- match
- end
- end
+ def find_object(project, id)
+ project.snippets.find_by(id: id)
end
- def url_for_snippet(snippet, project)
+ def url_for_object(snippet, project)
h = Gitlab::Application.routes.url_helpers
h.namespace_project_snippet_url(project.namespace, project, snippet,
only_path: context[:only_path])
diff --git a/lib/gitlab/markdown/upload_link_filter.rb b/lib/gitlab/markdown/upload_link_filter.rb
new file mode 100644
index 00000000000..fbada73ab86
--- /dev/null
+++ b/lib/gitlab/markdown/upload_link_filter.rb
@@ -0,0 +1,47 @@
+require 'gitlab/markdown'
+require 'html/pipeline/filter'
+require 'uri'
+
+module Gitlab
+ module Markdown
+ # HTML filter that "fixes" relative upload links to files.
+ # Context options:
+ # :project (required) - Current project
+ #
+ class UploadLinkFilter < HTML::Pipeline::Filter
+ def call
+ doc.search('a').each do |el|
+ process_link_attr el.attribute('href')
+ end
+
+ doc.search('img').each do |el|
+ process_link_attr el.attribute('src')
+ end
+
+ doc
+ end
+
+ protected
+
+ def process_link_attr(html_attr)
+ return if html_attr.blank?
+
+ uri = html_attr.value
+ if uri.starts_with?("/uploads/")
+ html_attr.value = build_url(uri).to_s
+ end
+ end
+
+ def build_url(uri)
+ File.join(Gitlab.config.gitlab.url, context[:project].path_with_namespace, uri)
+ end
+
+ # Ensure that a :project key exists in context
+ #
+ # Note that while the key might exist, its value could be nil!
+ def validate
+ needs :project
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb
index 6f436ea7167..0a20d9c0347 100644
--- a/lib/gitlab/markdown/user_reference_filter.rb
+++ b/lib/gitlab/markdown/user_reference_filter.rb
@@ -23,10 +23,39 @@ module Gitlab
end
end
+ def self.referenced_by(node)
+ if node.has_attribute?('data-group')
+ group = Group.find(node.attr('data-group')) rescue nil
+ return unless group
+
+ { user: group.users }
+ elsif node.has_attribute?('data-user')
+ { user: LazyReference.new(User, node.attr('data-user')) }
+ elsif node.has_attribute?('data-project')
+ project = Project.find(node.attr('data-project')) rescue nil
+ return unless project
+
+ { user: project.team.members.flatten }
+ end
+ end
+
+ def self.user_can_reference?(user, node, context)
+ if node.has_attribute?('data-group')
+ group = Group.find(node.attr('data-group')) rescue nil
+ Ability.abilities.allowed?(user, :read_group, group)
+ else
+ super
+ end
+ end
+
def call
replace_text_nodes_matching(User.reference_pattern) do |content|
user_link_filter(content)
end
+
+ replace_link_nodes_with_href(User.reference_pattern) do |link, text|
+ user_link_filter(link, link_text: text)
+ end
end
# Replace `@user` user references in text with links to the referenced
@@ -36,12 +65,12 @@ module Gitlab
#
# Returns a String with `@user` references replaced with links. All links
# have `gfm` and `gfm-project_member` class names attached for styling.
- def user_link_filter(text)
+ def user_link_filter(text, link_text: nil)
self.class.references_in(text) do |match, username|
if username == 'all'
- link_to_all
+ link_to_all(link_text: link_text)
elsif namespace = Namespace.find_by(path: username)
- link_to_namespace(namespace) || match
+ link_to_namespace(namespace, link_text: link_text) || match
else
match
end
@@ -58,51 +87,42 @@ module Gitlab
reference_class(:project_member)
end
- def link_to_all
+ def link_to_all(link_text: nil)
project = context[:project]
-
- # FIXME (rspeicher): Law of Demeter
- push_result(:user, *project.team.members.flatten)
-
url = urls.namespace_project_url(project.namespace, project,
only_path: context[:only_path])
+ data = data_attribute(project: project.id)
+ text = link_text || User.reference_prefix + 'all'
- text = User.reference_prefix + 'all'
- %(<a href="#{url}" class="#{link_class}">#{text}</a>)
+ link_tag(url, data, text)
end
- def link_to_namespace(namespace)
+ def link_to_namespace(namespace, link_text: nil)
if namespace.is_a?(Group)
- link_to_group(namespace.path, namespace)
+ link_to_group(namespace.path, namespace, link_text: link_text)
else
- link_to_user(namespace.path, namespace)
+ link_to_user(namespace.path, namespace, link_text: link_text)
end
end
- def link_to_group(group, namespace)
- return unless user_can_reference_group?(namespace)
-
- push_result(:user, *namespace.users)
-
+ def link_to_group(group, namespace, link_text: nil)
url = urls.group_url(group, only_path: context[:only_path])
- data = data_attribute(namespace.id, :group)
+ data = data_attribute(group: namespace.id)
+ text = link_text || Group.reference_prefix + group
- text = Group.reference_prefix + group
- %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>)
+ link_tag(url, data, text)
end
- def link_to_user(user, namespace)
- push_result(:user, namespace.owner)
-
+ def link_to_user(user, namespace, link_text: nil)
url = urls.user_url(user, only_path: context[:only_path])
- data = data_attribute(namespace.owner_id, :user)
+ data = data_attribute(user: namespace.owner_id)
+ text = link_text || User.reference_prefix + user
- text = User.reference_prefix + user
- %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>)
+ link_tag(url, data, text)
end
- def user_can_reference_group?(group)
- Ability.abilities.allowed?(context[:current_user], :read_group, group)
+ def link_tag(url, data, text)
+ %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>)
end
end
end
diff --git a/lib/gitlab/o_auth/provider.rb b/lib/gitlab/o_auth/provider.rb
index 90c3fe8da33..9ad7a38d505 100644
--- a/lib/gitlab/o_auth/provider.rb
+++ b/lib/gitlab/o_auth/provider.rb
@@ -1,6 +1,12 @@
module Gitlab
module OAuth
class Provider
+ LABELS = {
+ "github" => "GitHub",
+ "gitlab" => "GitLab.com",
+ "google_oauth2" => "Google"
+ }.freeze
+
def self.providers
Devise.omniauth_providers
end
@@ -23,8 +29,9 @@ module Gitlab
end
def self.label_for(name)
+ name = name.to_s
config = config_for(name)
- (config && config['label']) || name.to_s.titleize
+ (config && config['label']) || LABELS[name] || name.titleize
end
end
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 0dab7bcfa4d..70de6a74e76 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -9,7 +9,7 @@ module Gitlab
else
nil
end
- @query = Shellwords.shellescape(query) if query.present?
+ @query = query
end
def objects(scope, page = nil)
@@ -20,6 +20,8 @@ module Gitlab
Kaminari.paginate_array(blobs).page(page).per(per_page)
when 'wiki_blobs'
Kaminari.paginate_array(wiki_blobs).page(page).per(per_page)
+ when 'commits'
+ Kaminari.paginate_array(commits).page(page).per(per_page)
else
super
end
@@ -27,7 +29,7 @@ module Gitlab
def total_count
@total_count ||= issues_count + merge_requests_count + blobs_count +
- notes_count + wiki_blobs_count
+ notes_count + wiki_blobs_count + commits_count
end
def blobs_count
@@ -42,6 +44,10 @@ module Gitlab
@wiki_blobs_count ||= wiki_blobs.count
end
+ def commits_count
+ @commits_count ||= commits.count
+ end
+
private
def blobs
@@ -70,6 +76,14 @@ module Gitlab
Note.where(project_id: limit_project_ids).user.search(query).order('updated_at DESC')
end
+ def commits
+ if project.empty_repo? || query.blank?
+ []
+ else
+ project.repository.find_commits_by_message(query).compact
+ end
+ end
+
def limit_project_ids
[project.id]
end
diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb
index d010ade704e..4f9cdef3869 100644
--- a/lib/gitlab/push_data_builder.rb
+++ b/lib/gitlab/push_data_builder.rb
@@ -30,9 +30,12 @@ module Gitlab
# For performance purposes maximum 20 latest commits
# will be passed as post receive hook data.
- commit_attrs = commits_limited.map(&:hook_attrs)
+ commit_attrs = commits_limited.map do |commit|
+ commit.hook_attrs(with_changed_files: true)
+ end
type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push"
+
# Hash to be passed as post_receive_data
data = {
object_kind: type,
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 0961bd80421..3c3478a1271 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -3,11 +3,12 @@ require 'gitlab/markdown'
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor
- attr_accessor :project, :current_user
+ attr_accessor :project, :current_user, :load_lazy_references
- def initialize(project, current_user = nil)
+ def initialize(project, current_user = nil, load_lazy_references: true)
@project = project
@current_user = current_user
+ @load_lazy_references = load_lazy_references
end
def analyze(text)
@@ -26,9 +27,9 @@ module Gitlab
def references
@references ||= Hash.new do |references, type|
type = type.to_sym
- return references[type] if references.has_key?(type)
+ next references[type] if references.has_key?(type)
- references[type] = pipeline_result(type).uniq
+ references[type] = pipeline_result(type)
end
end
@@ -39,21 +40,42 @@ module Gitlab
#
# Returns the results Array for the requested filter type
def pipeline_result(filter_type)
- klass = filter_type.to_s.camelize + 'ReferenceFilter'
+ return [] if @text.blank?
+
+ klass = "#{filter_type.to_s.camelize}ReferenceFilter"
filter = Gitlab::Markdown.const_get(klass)
context = {
project: project,
current_user: current_user,
+
# We don't actually care about the links generated
only_path: true,
- ignore_blockquotes: true
+ ignore_blockquotes: true,
+
+ # ReferenceGathererFilter
+ load_lazy_references: false,
+ reference_filter: filter
}
- pipeline = HTML::Pipeline.new([filter], context)
+ # We need to autolink first to finds links to referables, and to prevent
+ # numeric anchors to be parsed as issue references.
+ filters = [
+ Gitlab::Markdown::AutolinkFilter,
+ filter,
+ Gitlab::Markdown::ReferenceGathererFilter
+ ]
+
+ pipeline = HTML::Pipeline.new(filters, context)
result = pipeline.call(@text)
- result[:references][filter_type]
+ values = result[:references][filter_type].uniq
+
+ if @load_lazy_references
+ values = Gitlab::Markdown::ReferenceFilter::LazyReference.load(values).uniq
+ end
+
+ values
end
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 9f1adc860d1..53ab2686b43 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -51,6 +51,23 @@ module Gitlab
"can contain only letters, digits, '_', '-' and '.'. "
end
+ def file_path_regex
+ @file_path_regex ||= /\A[a-zA-Z0-9_\-\.\/]*\z/.freeze
+ end
+
+ def file_path_regex_message
+ "can contain only letters, digits, '_', '-' and '.'. Separate directories with a '/'. "
+ end
+
+
+ def directory_traversal_regex
+ @directory_traversal_regex ||= /\.{2}/.freeze
+ end
+
+ def directory_traversal_regex_message
+ "cannot include directory traversal. "
+ end
+
def archive_formats_regex
# |zip|tar| tar.gz | tar.bz2 |
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 31aa3528c4c..2ef0e982256 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -14,7 +14,7 @@ module Gitlab
def self.mute_mailer
code = <<-eos
-def Notify.delay
+def Notify.deliver_later
self
end
eos
diff --git a/lib/gitlab/sherlock.rb b/lib/gitlab/sherlock.rb
new file mode 100644
index 00000000000..6360527a7aa
--- /dev/null
+++ b/lib/gitlab/sherlock.rb
@@ -0,0 +1,19 @@
+require 'securerandom'
+
+module Gitlab
+ module Sherlock
+ @collection = Collection.new
+
+ class << self
+ attr_reader :collection
+ end
+
+ def self.enabled?
+ Rails.env.development? && !!ENV['ENABLE_SHERLOCK']
+ end
+
+ def self.enable_line_profiler?
+ RUBY_ENGINE == 'ruby'
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/collection.rb b/lib/gitlab/sherlock/collection.rb
new file mode 100644
index 00000000000..66bd6258521
--- /dev/null
+++ b/lib/gitlab/sherlock/collection.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Sherlock
+ # A collection of transactions recorded by Sherlock.
+ #
+ # Method calls for this class are synchronized using a mutex to allow
+ # sharing of a single Collection instance between threads (e.g. when using
+ # Puma as a webserver).
+ class Collection
+ include Enumerable
+
+ def initialize
+ @transactions = []
+ @mutex = Mutex.new
+ end
+
+ def add(transaction)
+ synchronize { @transactions << transaction }
+ end
+
+ alias_method :<<, :add
+
+ def each(&block)
+ synchronize { @transactions.each(&block) }
+ end
+
+ def clear
+ synchronize { @transactions.clear }
+ end
+
+ def empty?
+ synchronize { @transactions.empty? }
+ end
+
+ def find_transaction(id)
+ find { |trans| trans.id == id }
+ end
+
+ def newest_first
+ sort { |a, b| b.finished_at <=> a.finished_at }
+ end
+
+ private
+
+ def synchronize(&block)
+ @mutex.synchronize(&block)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/file_sample.rb b/lib/gitlab/sherlock/file_sample.rb
new file mode 100644
index 00000000000..8a3e1a5e5bf
--- /dev/null
+++ b/lib/gitlab/sherlock/file_sample.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module Sherlock
+ class FileSample
+ attr_reader :id, :file, :line_samples, :events, :duration
+
+ # file - The full path to the file this sample belongs to.
+ # line_samples - An array of LineSample objects.
+ # duration - The total execution time in milliseconds.
+ # events - The total amount of events.
+ def initialize(file, line_samples, duration, events)
+ @id = SecureRandom.uuid
+ @file = file
+ @line_samples = line_samples
+ @duration = duration
+ @events = events
+ end
+
+ def relative_path
+ @relative_path ||= @file.gsub(/^#{Rails.root.to_s}\/?/, '')
+ end
+
+ def to_param
+ @id
+ end
+
+ def source
+ @source ||= File.read(@file)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/line_profiler.rb b/lib/gitlab/sherlock/line_profiler.rb
new file mode 100644
index 00000000000..aa1468bff6b
--- /dev/null
+++ b/lib/gitlab/sherlock/line_profiler.rb
@@ -0,0 +1,98 @@
+module Gitlab
+ module Sherlock
+ # Class for profiling code on a per line basis.
+ #
+ # The LineProfiler class can be used to profile code on per line basis
+ # without littering your code with Ruby implementation specific profiling
+ # methods.
+ #
+ # This profiler only includes samples taking longer than a given threshold
+ # and those that occur in the actual application (e.g. files from Gems are
+ # ignored).
+ class LineProfiler
+ # The minimum amount of time that has to be spent in a file for it to be
+ # included in a list of samples.
+ MINIMUM_DURATION = 10.0
+
+ # Profiles the given block.
+ #
+ # Example:
+ #
+ # profiler = LineProfiler.new
+ #
+ # retval, samples = profiler.profile do
+ # "cats are amazing"
+ # end
+ #
+ # retval # => "cats are amazing"
+ # samples # => [#<Gitlab::Sherlock::FileSample ...>, ...]
+ #
+ # Returns an Array containing the block's return value and an Array of
+ # FileSample objects.
+ def profile(&block)
+ if mri?
+ profile_mri(&block)
+ else
+ raise NotImplementedError,
+ 'Line profiling is not supported on this platform'
+ end
+ end
+
+ # Profiles the given block using rblineprof (MRI only).
+ def profile_mri
+ require 'rblineprof'
+
+ retval = nil
+ samples = lineprof(/^#{Rails.root.to_s}/) { retval = yield }
+
+ file_samples = aggregate_rblineprof(samples)
+
+ [retval, file_samples]
+ end
+
+ # Returns an Array of file samples based on the output of rblineprof.
+ #
+ # lineprof_stats - A Hash containing rblineprof statistics on a per file
+ # basis.
+ #
+ # Returns an Array of FileSample objects.
+ def aggregate_rblineprof(lineprof_stats)
+ samples = []
+
+ lineprof_stats.each do |(file, stats)|
+ source_lines = File.read(file).each_line.to_a
+ line_samples = []
+
+ total_duration = microsec_to_millisec(stats[0][0])
+ total_events = stats[0][2]
+
+ next if total_duration <= MINIMUM_DURATION
+
+ stats[1..-1].each_with_index do |data, index|
+ next unless source_lines[index]
+
+ duration = microsec_to_millisec(data[0])
+ events = data[2]
+
+ line_samples << LineSample.new(duration, events)
+ end
+
+ samples << FileSample.
+ new(file, line_samples, total_duration, total_events)
+ end
+
+ samples
+ end
+
+ private
+
+ def microsec_to_millisec(microsec)
+ microsec / 1000.0
+ end
+
+ def mri?
+ RUBY_ENGINE == 'ruby'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/line_sample.rb b/lib/gitlab/sherlock/line_sample.rb
new file mode 100644
index 00000000000..eb1948eb6d6
--- /dev/null
+++ b/lib/gitlab/sherlock/line_sample.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Sherlock
+ class LineSample
+ attr_reader :duration, :events
+
+ # duration - The execution time in milliseconds.
+ # events - The amount of events.
+ def initialize(duration, events)
+ @duration = duration
+ @events = events
+ end
+
+ # Returns the sample duration percentage relative to the given duration.
+ #
+ # Example:
+ #
+ # sample.duration # => 150
+ # sample.percentage_of(1500) # => 10.0
+ #
+ # total_duration - The total duration to compare with.
+ #
+ # Returns a float
+ def percentage_of(total_duration)
+ (duration.to_f / total_duration) * 100.0
+ end
+
+ # Returns true if the current sample takes up the majority of the given
+ # duration.
+ #
+ # total_duration - The total duration to compare with.
+ def majority_of?(total_duration)
+ percentage_of(total_duration) >= 30
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/location.rb b/lib/gitlab/sherlock/location.rb
new file mode 100644
index 00000000000..5ac265618ad
--- /dev/null
+++ b/lib/gitlab/sherlock/location.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module Sherlock
+ class Location
+ attr_reader :path, :line
+
+ SHERLOCK_DIR = File.dirname(__FILE__)
+
+ # Creates a new Location from a `Thread::Backtrace::Location`.
+ def self.from_ruby_location(location)
+ new(location.path, location.lineno)
+ end
+
+ # path - The full path of the frame as a String.
+ # line - The line number of the frame as a Fixnum.
+ def initialize(path, line)
+ @path = path
+ @line = line
+ end
+
+ # Returns true if the current frame originated from the application.
+ def application?
+ @path.start_with?(Rails.root.to_s) && !path.start_with?(SHERLOCK_DIR)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/middleware.rb b/lib/gitlab/sherlock/middleware.rb
new file mode 100644
index 00000000000..687332fc5fc
--- /dev/null
+++ b/lib/gitlab/sherlock/middleware.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module Sherlock
+ # Rack middleware used for tracking request metrics.
+ class Middleware
+ CONTENT_TYPES = /text\/html|application\/json/i
+
+ IGNORE_PATHS = %r{^/sherlock}
+
+ def initialize(app)
+ @app = app
+ end
+
+ # env - A Hash containing Rack environment details.
+ def call(env)
+ if instrument?(env)
+ call_with_instrumentation(env)
+ else
+ @app.call(env)
+ end
+ end
+
+ def call_with_instrumentation(env)
+ trans = transaction_from_env(env)
+ retval = trans.run { @app.call(env) }
+
+ Sherlock.collection.add(trans)
+
+ retval
+ end
+
+ def instrument?(env)
+ !!(env['HTTP_ACCEPT'] =~ CONTENT_TYPES &&
+ env['REQUEST_URI'] !~ IGNORE_PATHS)
+ end
+
+ def transaction_from_env(env)
+ Transaction.new(env['REQUEST_METHOD'], env['REQUEST_URI'])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb
new file mode 100644
index 00000000000..4917c4ae2ac
--- /dev/null
+++ b/lib/gitlab/sherlock/query.rb
@@ -0,0 +1,114 @@
+module Gitlab
+ module Sherlock
+ class Query
+ attr_reader :id, :query, :started_at, :finished_at, :backtrace
+
+ # SQL identifiers that should be prefixed with newlines.
+ PREFIX_NEWLINE = /
+ \s+(FROM
+ |(LEFT|RIGHT)?INNER\s+JOIN
+ |(LEFT|RIGHT)?OUTER\s+JOIN
+ |WHERE
+ |AND
+ |GROUP\s+BY
+ |ORDER\s+BY
+ |LIMIT
+ |OFFSET)\s+/ix # Vim indent breaks when this is on a newline :<
+
+ # Creates a new Query using a String and a separate Array of bindings.
+ #
+ # query - A String containing a SQL query, optionally with numeric
+ # placeholders (`$1`, `$2`, etc).
+ #
+ # bindings - An Array of ActiveRecord columns and their values.
+ # started_at - The start time of the query as a Time-like object.
+ # finished_at - The completion time of the query as a Time-like object.
+ #
+ # Returns a new Query object.
+ def self.new_with_bindings(query, bindings, started_at, finished_at)
+ bindings.each_with_index do |(_, value), index|
+ quoted_value = ActiveRecord::Base.connection.quote(value)
+
+ query = query.gsub("$#{index + 1}", quoted_value)
+ end
+
+ new(query, started_at, finished_at)
+ end
+
+ # query - The SQL query as a String (without placeholders).
+ # started_at - The start time of the query as a Time-like object.
+ # finished_at - The completion time of the query as a Time-like object.
+ def initialize(query, started_at, finished_at)
+ @id = SecureRandom.uuid
+ @query = query
+ @started_at = started_at
+ @finished_at = finished_at
+ @backtrace = caller_locations.map do |loc|
+ Location.from_ruby_location(loc)
+ end
+
+ unless @query.end_with?(';')
+ @query += ';'
+ end
+ end
+
+ # Returns the query duration in milliseconds.
+ def duration
+ @duration ||= (@finished_at - @started_at) * 1000.0
+ end
+
+ def to_param
+ @id
+ end
+
+ # Returns a human readable version of the query.
+ def formatted_query
+ @formatted_query ||= format_sql(@query)
+ end
+
+ # Returns the last application frame of the backtrace.
+ def last_application_frame
+ @last_application_frame ||= @backtrace.find(&:application?)
+ end
+
+ # Returns an Array of application frames (excluding Gems and the likes).
+ def application_backtrace
+ @application_backtrace ||= @backtrace.select(&:application?)
+ end
+
+ # Returns the query plan as a String.
+ def explain
+ unless @explain
+ ActiveRecord::Base.connection.transaction do
+ @explain = raw_explain(@query).values.flatten.join("\n")
+
+ # Roll back any queries that mutate data so we don't mess up
+ # anything when running explain on an INSERT, UPDATE, DELETE, etc.
+ raise ActiveRecord::Rollback
+ end
+ end
+
+ @explain
+ end
+
+ private
+
+ def raw_explain(query)
+ if Gitlab::Database.postgresql?
+ explain = "EXPLAIN ANALYZE #{query};"
+ else
+ explain = "EXPLAIN #{query};"
+ end
+
+ ActiveRecord::Base.connection.execute(explain)
+ end
+
+ def format_sql(query)
+ query.each_line.
+ map { |line| line.strip }.
+ join("\n").
+ gsub(PREFIX_NEWLINE) { "\n#{$1} " }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/transaction.rb b/lib/gitlab/sherlock/transaction.rb
new file mode 100644
index 00000000000..3489fb251b6
--- /dev/null
+++ b/lib/gitlab/sherlock/transaction.rb
@@ -0,0 +1,136 @@
+module Gitlab
+ module Sherlock
+ class Transaction
+ attr_reader :id, :type, :path, :queries, :file_samples, :started_at,
+ :finished_at, :view_counts
+
+ # type - The type of transaction (e.g. "GET", "POST", etc)
+ # path - The path of the transaction (e.g. the HTTP request path)
+ def initialize(type, path)
+ @id = SecureRandom.uuid
+ @type = type
+ @path = path
+ @queries = []
+ @file_samples = []
+ @started_at = nil
+ @finished_at = nil
+ @thread = Thread.current
+ @view_counts = Hash.new(0)
+ end
+
+ # Runs the transaction and returns the block's return value.
+ def run
+ @started_at = Time.now
+
+ retval = with_subscriptions do
+ profile_lines { yield }
+ end
+
+ @finished_at = Time.now
+
+ retval
+ end
+
+ # Returns the duration in seconds.
+ def duration
+ @duration ||= started_at && finished_at ? finished_at - started_at : 0
+ end
+
+ # Returns the total query duration in seconds.
+ def query_duration
+ @query_duration ||= @queries.map { |q| q.duration }.inject(:+) / 1000.0
+ end
+
+ def to_param
+ @id
+ end
+
+ # Returns the queries sorted in descending order by their durations.
+ def sorted_queries
+ @queries.sort { |a, b| b.duration <=> a.duration }
+ end
+
+ # Returns the file samples sorted in descending order by their durations.
+ def sorted_file_samples
+ @file_samples.sort { |a, b| b.duration <=> a.duration }
+ end
+
+ # Finds a query by the given ID.
+ #
+ # id - The query ID as a String.
+ #
+ # Returns a Query object if one could be found, nil otherwise.
+ def find_query(id)
+ @queries.find { |query| query.id == id }
+ end
+
+ # Finds a file sample by the given ID.
+ #
+ # id - The query ID as a String.
+ #
+ # Returns a FileSample object if one could be found, nil otherwise.
+ def find_file_sample(id)
+ @file_samples.find { |sample| sample.id == id }
+ end
+
+ def profile_lines
+ retval = nil
+
+ if Sherlock.enable_line_profiler?
+ retval, @file_samples = LineProfiler.new.profile { yield }
+ else
+ retval = yield
+ end
+
+ retval
+ end
+
+ def subscribe_to_active_record
+ ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data|
+ next unless same_thread?
+
+ track_query(data[:sql].strip, data[:binds], start, finish)
+ end
+ end
+
+ def subscribe_to_action_view
+ regex = /render_(template|partial)\.action_view/
+
+ ActiveSupport::Notifications.subscribe(regex) do |_, start, finish, _, data|
+ next unless same_thread?
+
+ track_view(data[:identifier])
+ end
+ end
+
+ private
+
+ def track_query(query, bindings, start, finish)
+ @queries << Query.new_with_bindings(query, bindings, start, finish)
+ end
+
+ def track_view(path)
+ @view_counts[path] += 1
+ end
+
+ def with_subscriptions
+ ar_subscriber = subscribe_to_active_record
+ av_subscriber = subscribe_to_action_view
+
+ retval = yield
+
+ ActiveSupport::Notifications.unsubscribe(ar_subscriber)
+ ActiveSupport::Notifications.unsubscribe(av_subscriber)
+
+ retval
+ end
+
+ # In case somebody uses a multi-threaded server locally (e.g. Puma) we
+ # _only_ want to track notifications that originate from the transaction
+ # thread.
+ def same_thread?
+ Thread.current == @thread
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb
new file mode 100644
index 00000000000..1cd89b3a9c4
--- /dev/null
+++ b/lib/gitlab/sql/union.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ module SQL
+ # Class for building SQL UNION statements.
+ #
+ # ORDER BYs are dropped from the relations as the final sort order is not
+ # guaranteed any way.
+ #
+ # Example usage:
+ #
+ # union = Gitlab::SQL::Union.new(user.personal_projects, user.projects)
+ # sql = union.to_sql
+ #
+ # Project.where("id IN (#{sql})")
+ class Union
+ def initialize(relations)
+ @relations = relations
+ end
+
+ def to_sql
+ # Some relations may include placeholders for prepared statements, these
+ # aren't incremented properly when joining relations together this way.
+ # By using "unprepared_statements" we remove the usage of placeholders
+ # (thus fixing this problem), at a slight performance cost.
+ fragments = ActiveRecord::Base.connection.unprepared_statement do
+ @relations.map do |rel|
+ rel.reorder(nil).to_sql
+ end
+ end
+
+ fragments.join("\nUNION\n")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb
index cf040971c6e..f3567f3ef85 100644
--- a/lib/gitlab/upgrader.rb
+++ b/lib/gitlab/upgrader.rb
@@ -50,15 +50,15 @@ module Gitlab
end
def fetch_git_tags
- remote_tags, _ = Gitlab::Popen.popen(%W(git ls-remote --tags https://gitlab.com/gitlab-org/gitlab-ce.git))
+ remote_tags, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} ls-remote --tags https://gitlab.com/gitlab-org/gitlab-ce.git))
remote_tags.split("\n").grep(/tags\/v#{current_version.major}/)
end
def update_commands
{
- "Stash changed files" => %W(git stash),
- "Get latest code" => %W(git fetch),
- "Switch to new version" => %W(git checkout v#{latest_version}),
+ "Stash changed files" => %W(#{Gitlab.config.git.bin_path} stash),
+ "Get latest code" => %W(#{Gitlab.config.git.bin_path} fetch),
+ "Switch to new version" => %W(#{Gitlab.config.git.bin_path} checkout v#{latest_version}),
"Install gems" => %W(bundle),
"Migrate DB" => %W(bundle exec rake db:migrate),
"Recompile assets" => %W(bundle exec rake assets:clean assets:precompile),
diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb
new file mode 100644
index 00000000000..be8fcc7b2d2
--- /dev/null
+++ b/lib/gitlab/uploads_transfer.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ class UploadsTransfer
+ def move_project(project_path, namespace_path_was, namespace_path)
+ new_namespace_folder = File.join(root_dir, namespace_path)
+ FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder)
+ from = File.join(root_dir, namespace_path_was, project_path)
+ to = File.join(root_dir, namespace_path, project_path)
+ move(from, to, "")
+ end
+
+ def rename_project(path_was, path, namespace_path)
+ base_dir = File.join(root_dir, namespace_path)
+ move(path_was, path, base_dir)
+ end
+
+ def rename_namespace(path_was, path)
+ move(path_was, path)
+ end
+
+ private
+
+ def move(path_was, path, base_dir = nil)
+ base_dir = root_dir unless base_dir
+ from = File.join(base_dir, path_was)
+ to = File.join(base_dir, path)
+ FileUtils.mv(from, to)
+ rescue Errno::ENOENT
+ false
+ end
+
+ def root_dir
+ File.join(Rails.root, "public", "uploads")
+ end
+ end
+end
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index a80e7e77430..43fda6fa92e 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -37,10 +37,9 @@ web_server_pid_path="$pid_path/unicorn.pid"
sidekiq_pid_path="$pid_path/sidekiq.pid"
mail_room_enabled=false
mail_room_pid_path="$pid_path/mail_room.pid"
-gitlab_git_http_server_pid_path="$pid_path/gitlab-git-http-server.pid"
-gitlab_git_http_server_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-git-http-server.socket -authBackend http://127.0.0.1:8080"
-gitlab_git_http_server_repo_root='/home/git/repositories'
-gitlab_git_http_server_log="$app_root/log/gitlab-git-http-server.log"
+gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
+gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080"
+gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
shell_path="/bin/bash"
# Read configuration variable file if it is present
@@ -76,8 +75,8 @@ check_pids(){
else
spid=0
fi
- if [ -f "$gitlab_git_http_server_pid_path" ]; then
- hpid=$(cat "$gitlab_git_http_server_pid_path")
+ if [ -f "$gitlab_workhorse_pid_path" ]; then
+ hpid=$(cat "$gitlab_workhorse_pid_path")
else
hpid=0
fi
@@ -94,7 +93,7 @@ check_pids(){
wait_for_pids(){
# We are sleeping a bit here mostly because sidekiq is slow at writing it's pid
i=0;
- while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_git_http_server_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do
+ while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do
sleep 0.1;
i=$((i+1))
if [ $((i%10)) = 0 ]; then
@@ -131,9 +130,9 @@ check_status(){
fi
if [ $hpid -ne 0 ]; then
kill -0 "$hpid" 2>/dev/null
- gitlab_git_http_server_status="$?"
+ gitlab_workhorse_status="$?"
else
- gitlab_git_http_server_status="-1"
+ gitlab_workhorse_status="-1"
fi
if [ "$mail_room_enabled" = true ]; then
if [ $mpid -ne 0 ]; then
@@ -143,7 +142,7 @@ check_status(){
mail_room_status="-1"
fi
fi
- if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_git_http_server_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then
+ if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then
gitlab_status=0
else
# http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
@@ -171,9 +170,9 @@ check_stale_pids(){
exit 1
fi
fi
- if [ "$hpid" != "0" ] && [ "$gitlab_git_http_server_status" != "0" ]; then
- echo "Removing stale gitlab-git-http-server pid. This is most likely caused by gitlab-git-http-server crashing the last time it ran."
- if ! rm "$gitlab_git_http_server_pid_path"; then
+ if [ "$hpid" != "0" ] && [ "$gitlab_workhorse_status" != "0" ]; then
+ echo "Removing stale gitlab-workhorse pid. This is most likely caused by gitlab-workhorse crashing the last time it ran."
+ if ! rm "$gitlab_workhorse_pid_path"; then
echo "Unable to remove stale pid, exiting"
exit 1
fi
@@ -190,7 +189,7 @@ check_stale_pids(){
## If no parts of the service is running, bail out.
exit_if_not_running(){
check_stale_pids
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_git_http_server_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
echo "GitLab is not running."
exit
fi
@@ -206,8 +205,8 @@ start_gitlab() {
if [ "$sidekiq_status" != "0" ]; then
echo "Starting GitLab Sidekiq"
fi
- if [ "$gitlab_git_http_server_status" != "0" ]; then
- echo "Starting gitlab-git-http-server"
+ if [ "$gitlab_workhorse_status" != "0" ]; then
+ echo "Starting gitlab-workhorse"
fi
if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then
echo "Starting GitLab MailRoom"
@@ -230,15 +229,14 @@ start_gitlab() {
RAILS_ENV=$RAILS_ENV bin/background_jobs start &
fi
- if [ "$gitlab_git_http_server_status" = "0" ]; then
- echo "The gitlab-git-http-server is already running with pid $spid, not restarting"
+ if [ "$gitlab_workhorse_status" = "0" ]; then
+ echo "The gitlab-workhorse is already running with pid $spid, not restarting"
else
- # No need to remove a socket, gitlab-git-http-server does this itself
- $app_root/bin/daemon_with_pidfile $gitlab_git_http_server_pid_path \
- $app_root/../gitlab-git-http-server/gitlab-git-http-server \
- $gitlab_git_http_server_options \
- $gitlab_git_http_server_repo_root \
- >> $gitlab_git_http_server_log 2>&1 &
+ # No need to remove a socket, gitlab-workhorse does this itself
+ $app_root/bin/daemon_with_pidfile $gitlab_workhorse_pid_path \
+ $app_root/../gitlab-workhorse/gitlab-workhorse \
+ $gitlab_workhorse_options \
+ >> $gitlab_workhorse_log 2>&1 &
fi
if [ "$mail_room_enabled" = true ]; then
@@ -268,9 +266,9 @@ stop_gitlab() {
echo "Shutting down GitLab Sidekiq"
RAILS_ENV=$RAILS_ENV bin/background_jobs stop
fi
- if [ "$gitlab_git_http_server_status" = "0" ]; then
- echo "Shutting down gitlab-git-http-server"
- kill -- $(cat $gitlab_git_http_server_pid_path)
+ if [ "$gitlab_workhorse_status" = "0" ]; then
+ echo "Shutting down gitlab-workhorse"
+ kill -- $(cat $gitlab_workhorse_pid_path)
fi
if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; then
echo "Shutting down GitLab MailRoom"
@@ -278,11 +276,11 @@ stop_gitlab() {
fi
# If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script.
- while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_git_http_server_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do
+ while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do
sleep 1
check_status
printf "."
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_git_http_server_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
printf "\n"
break
fi
@@ -292,7 +290,7 @@ stop_gitlab() {
# Cleaning up unused pids
rm "$web_server_pid_path" 2>/dev/null
# rm "$sidekiq_pid_path" 2>/dev/null # Sidekiq seems to be cleaning up it's own pid.
- rm -f "$gitlab_git_http_server_pid_path"
+ rm -f "$gitlab_workhorse_pid_path"
if [ "$mail_room_enabled" = true ]; then
rm "$mail_room_pid_path" 2>/dev/null
fi
@@ -303,7 +301,7 @@ stop_gitlab() {
## Prints the status of GitLab and it's components.
print_status() {
check_status
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_git_http_server_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
echo "GitLab is not running."
return
fi
@@ -317,10 +315,10 @@ print_status() {
else
printf "The GitLab Sidekiq job dispatcher is \033[31mnot running\033[0m.\n"
fi
- if [ "$gitlab_git_http_server_status" = "0" ]; then
- echo "The gitlab-git-http-server with pid $hpid is running."
+ if [ "$gitlab_workhorse_status" = "0" ]; then
+ echo "The gitlab-workhorse with pid $hpid is running."
else
- printf "The gitlab-git-http-server is \033[31mnot running\033[0m.\n"
+ printf "The gitlab-workhorse is \033[31mnot running\033[0m.\n"
fi
if [ "$mail_room_enabled" = true ]; then
if [ "$mail_room_status" = "0" ]; then
@@ -329,7 +327,7 @@ print_status() {
printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n"
fi
fi
- if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then
+ if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then
printf "GitLab and all its components are \033[32mup and running\033[0m.\n"
fi
}
@@ -360,7 +358,7 @@ reload_gitlab(){
## Restarts Sidekiq and Unicorn.
restart_gitlab(){
check_status
- if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_git_http_server" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then
+ if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then
stop_gitlab
fi
start_gitlab
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index aab5acaa72c..79ae8e0ae55 100755
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -30,15 +30,14 @@ web_server_pid_path="$pid_path/unicorn.pid"
# The default is "$pid_path/sidekiq.pid"
sidekiq_pid_path="$pid_path/sidekiq.pid"
-gitlab_git_http_server_pid_path="$pid_path/gitlab-git-http-server.pid"
-# The -listenXxx settings determine where gitlab-git-http-server
+gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
+# The -listenXxx settings determine where gitlab-workhorse
# listens for connections from NGINX. To listen on localhost:8181, write
# '-listenNetwork tcp -listenAddr localhost:8181'.
-# The -authBackend setting tells gitlab-git-http-server where it can reach
+# The -authBackend setting tells gitlab-workhorse where it can reach
# Unicorn.
-gitlab_git_http_server_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-git-http-server.socket -authBackend http://127.0.0.1:8080"
-gitlab_git_http_server_repo_root="/home/git/repositories"
-gitlab_git_http_server_log="$app_root/log/gitlab-git-http-server.log"
+gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080"
+gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
# mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled.
# This is required for the Reply by email feature.
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 7218a4d2f20..2a79fbdcf93 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -38,13 +38,13 @@ upstream gitlab {
server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0;
}
-upstream gitlab-git-http-server {
- server unix:/home/git/gitlab/tmp/sockets/gitlab-git-http-server.socket fail_timeout=0;
+upstream gitlab-workhorse {
+ server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
}
## Normal HTTP host
server {
- ## Either remove "default_server" from the listen line below,
+ ## Either remove "default_server" from the listen line below,
## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
## to be served if you visit any address that your server responds to, eg.
## the ip address of the server (http://x.x.x.x/)n 0.0.0.0:80 default_server;
@@ -67,7 +67,7 @@ server {
location / {
## Serve static files from defined root folder.
## @gitlab is a named location for the upstream fallback, see below.
- try_files $uri $uri/index.html $uri.html @gitlab;
+ try_files $uri /index.html $uri.html @gitlab;
}
## We route uploads through GitLab to prevent XSS and enforce access control.
@@ -113,7 +113,52 @@ server {
proxy_pass http://gitlab;
}
- location ~ [-\/\w\.]+\.git\/ {
+ location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
+ location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
+ location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
+ location ~ ^/api/v3/projects/.*/repository/archive {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
+ # Build artifacts should be submitted to this location
+ location ~ ^/[\w\.-]+/[\w\.-]+/builds/download {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
+ # Build artifacts should be submitted to this location
+ location ~ /ci/api/v1/builds/[0-9]+/artifacts {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
+ location @gitlab-workhorse {
+ client_max_body_size 0;
## If you use HTTPS make sure you disable gzip compression
## to be safe against BREACH attack.
# gzip off;
@@ -129,7 +174,7 @@ server {
# The following settings only work with NGINX 1.7.11 or newer
#
- # # Pass chunked request bodies to gitlab-git-http-server as-is
+ # # Pass chunked request bodies to gitlab-workhorse as-is
# proxy_request_buffering off;
# proxy_http_version 1.1;
@@ -138,7 +183,7 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
- proxy_pass http://gitlab-git-http-server;
+ proxy_pass http://gitlab-workhorse;
}
## Enable gzip compression as per rails guide:
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 7dabfba87e2..79fe1474821 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -42,13 +42,13 @@ upstream gitlab {
server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0;
}
-upstream gitlab-git-http-server {
- server unix:/home/git/gitlab/tmp/sockets/gitlab-git-http-server.socket fail_timeout=0;
+upstream gitlab-workhorse {
+ server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
}
## Redirects all HTTP traffic to the HTTPS host
server {
- ## Either remove "default_server" from the listen line below,
+ ## Either remove "default_server" from the listen line below,
## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
## to be served if you visit any address that your server responds to, eg.
## the ip address of the server (http://x.x.x.x/)
@@ -56,7 +56,7 @@ server {
listen [::]:80 ipv6only=on default_server;
server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com
server_tokens off; ## Don't show the nginx version number, a security best practice
- return 301 https://$server_name$request_uri;
+ return 301 https://$http_host$request_uri;
access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log;
}
@@ -112,7 +112,7 @@ server {
location / {
## Serve static files from defined root folder.
## @gitlab is a named location for the upstream fallback, see below.
- try_files $uri $uri/index.html $uri.html @gitlab;
+ try_files $uri /index.html $uri.html @gitlab;
}
## We route uploads through GitLab to prevent XSS and enforce access control.
@@ -160,7 +160,52 @@ server {
proxy_pass http://gitlab;
}
- location ~ [-\/\w\.]+\.git\/ {
+ location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
+ location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
+ location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
+ location ~ ^/api/v3/projects/.*/repository/archive {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
+ # Build artifacts should be submitted to this location
+ location ~ ^/[\w\.-]+/[\w\.-]+/builds/download {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
+ # Build artifacts should be submitted to this location
+ location ~ /ci/api/v1/builds/[0-9]+/artifacts {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
+ location @gitlab-workhorse {
+ client_max_body_size 0;
## If you use HTTPS make sure you disable gzip compression
## to be safe against BREACH attack.
gzip off;
@@ -176,7 +221,7 @@ server {
# The following settings only work with NGINX 1.7.11 or newer
#
- # # Pass chunked request bodies to gitlab-git-http-server as-is
+ # # Pass chunked request bodies to gitlab-workhorse as-is
# proxy_request_buffering off;
# proxy_http_version 1.1;
@@ -185,7 +230,7 @@ server {
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
- proxy_pass http://gitlab-git-http-server;
+ proxy_pass http://gitlab-workhorse;
}
## Enable gzip compression as per rails guide:
diff --git a/lib/tasks/ci/migrate.rake b/lib/tasks/ci/migrate.rake
deleted file mode 100644
index 1de664c85e1..00000000000
--- a/lib/tasks/ci/migrate.rake
+++ /dev/null
@@ -1,87 +0,0 @@
-namespace :ci do
- desc 'GitLab | Import and migrate CI database'
- task migrate: :environment do
- warn_user_is_not_gitlab
- configure_cron_mode
-
- unless ENV['force'] == 'yes'
- puts 'This will remove all CI related data and restore it from the provided backup.'
- ask_to_continue
- puts ''
- end
-
- # disable CI for time of migration
- enable_ci(false)
-
- # unpack archives
- migrate = Ci::Migrate::Manager.new
- migrate.unpack
-
- Rake::Task['ci:migrate:db'].invoke
- Rake::Task['ci:migrate:builds'].invoke
- Rake::Task['ci:migrate:tags'].invoke
- Rake::Task['ci:migrate:services'].invoke
-
- # enable CI for time of migration
- enable_ci(true)
-
- migrate.cleanup
- end
-
- namespace :migrate do
- desc 'GitLab | Import CI database'
- task db: :environment do
- configure_cron_mode
- $progress.puts 'Restoring database ... '.blue
- Ci::Migrate::Database.new.restore
- $progress.puts 'done'.green
- end
-
- desc 'GitLab | Import CI builds'
- task builds: :environment do
- configure_cron_mode
- $progress.puts 'Restoring builds ... '.blue
- Ci::Migrate::Builds.new.restore
- $progress.puts 'done'.green
- end
-
- desc 'GitLab | Migrate CI tags'
- task tags: :environment do
- configure_cron_mode
- $progress.puts 'Migrating tags ... '.blue
- ::Ci::Migrate::Tags.new.restore
- $progress.puts 'done'.green
- end
-
- desc 'GitLab | Migrate CI auto-increments'
- task autoincrements: :environment do
- c = ActiveRecord::Base.connection
- c.tables.select { |t| t.start_with?('ci_') }.each do |table|
- result = c.select_one("SELECT id FROM #{table} ORDER BY id DESC LIMIT 1")
- if result
- ai_val = result['id'].to_i + 1
- puts "Resetting auto increment ID for #{table} to #{ai_val}"
- if c.adapter_name == 'PostgreSQL'
- c.execute("ALTER SEQUENCE #{table}_id_seq RESTART WITH #{ai_val}")
- else
- c.execute("ALTER TABLE #{table} AUTO_INCREMENT = #{ai_val}")
- end
- end
- end
- end
-
- desc 'GitLab | Migrate CI services'
- task services: :environment do
- $progress.puts 'Migrating services ... '.blue
- c = ActiveRecord::Base.connection
- c.execute("UPDATE ci_services SET type=CONCAT('Ci::', type) WHERE type NOT LIKE 'Ci::%'")
- $progress.puts 'done'.green
- end
- end
-
- def enable_ci(enabled)
- settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults
- settings.ci_enabled = enabled
- settings.save!
- end
-end
diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake
new file mode 100644
index 00000000000..e9587595fef
--- /dev/null
+++ b/lib/tasks/flay.rake
@@ -0,0 +1,9 @@
+desc 'Code duplication analyze via flay'
+task :flay do
+ output = %x(bundle exec flay --mass 35 app/ lib/gitlab/)
+
+ if output.include? "Similar code found"
+ puts output
+ exit 1
+ end
+end
diff --git a/lib/tasks/flog.rake b/lib/tasks/flog.rake
new file mode 100644
index 00000000000..3bfe999ae74
--- /dev/null
+++ b/lib/tasks/flog.rake
@@ -0,0 +1,25 @@
+desc 'Code complexity analyze via flog'
+task :flog do
+ output = %x(bundle exec flog -m app/ lib/gitlab)
+ exit_code = 0
+ minimum_score = 70
+ output = output.lines
+
+ # Skip total complexity score
+ output.shift
+
+ # Skip some trash info
+ output.shift
+
+ output.each do |line|
+ score, method = line.split(" ")
+ score = score.to_i
+
+ if score > minimum_score
+ exit_code = 1
+ puts "High complexity in #{method}. Score: #{score}"
+ end
+ end
+
+ exit exit_code
+end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index f20c7f71ba5..cb4abe13799 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -12,6 +12,8 @@ namespace :gitlab do
Rake::Task["gitlab:backup:repo:create"].invoke
Rake::Task["gitlab:backup:uploads:create"].invoke
Rake::Task["gitlab:backup:builds:create"].invoke
+ Rake::Task["gitlab:backup:artifacts:create"].invoke
+ Rake::Task["gitlab:backup:lfs:create"].invoke
backup = Backup::Manager.new
backup.pack
@@ -32,6 +34,8 @@ namespace :gitlab do
Rake::Task["gitlab:backup:repo:restore"].invoke unless backup.skipped?("repositories")
Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads")
Rake::Task["gitlab:backup:builds:restore"].invoke unless backup.skipped?("builds")
+ Rake::Task["gitlab:backup:artifacts:restore"].invoke unless backup.skipped?("artifacts")
+ Rake::Task["gitlab:backup:lfs:restore"].invoke unless backup.skipped?("lfs")
Rake::Task["gitlab:shell:setup"].invoke
backup.cleanup
@@ -113,6 +117,44 @@ namespace :gitlab do
end
end
+ namespace :artifacts do
+ task create: :environment do
+ $progress.puts "Dumping artifacts ... ".blue
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("artifacts")
+ $progress.puts "[SKIPPED]".cyan
+ else
+ Backup::Artifacts.new.dump
+ $progress.puts "done".green
+ end
+ end
+
+ task restore: :environment do
+ $progress.puts "Restoring artifacts ... ".blue
+ Backup::Artifacts.new.restore
+ $progress.puts "done".green
+ end
+ end
+
+ namespace :lfs do
+ task create: :environment do
+ $progress.puts "Dumping lfs objects ... ".blue
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("lfs")
+ $progress.puts "[SKIPPED]".cyan
+ else
+ Backup::Lfs.new.dump
+ $progress.puts "done".green
+ end
+ end
+
+ task restore: :environment do
+ $progress.puts "Restoring lfs objects ... ".blue
+ Backup::Lfs.new.restore
+ $progress.puts "done".green
+ end
+ end
+
def configure_cron_mode
if ENV['CRON']
# We need an object we can say 'puts' and 'print' to; let's use a
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 66f1ecf385f..a25fac62cfc 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -335,7 +335,7 @@ namespace :gitlab do
print "Redis version >= #{min_redis_version}? ... "
redis_version = run(%W(redis-cli --version))
- redis_version = redis_version.try(:match, /redis-cli (.*)/)
+ redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
if redis_version &&
(Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
puts "yes".green
@@ -642,7 +642,6 @@ namespace :gitlab do
if Gitlab.config.incoming_email.enabled
check_address_formatted_correctly
- check_mail_room_config_exists
check_imap_authentication
if Rails.env.production?
@@ -744,42 +743,16 @@ namespace :gitlab do
end
end
- def check_mail_room_config_exists
- print "MailRoom config exists? ... "
-
- mail_room_config_file = Rails.root.join("config", "mail_room.yml")
-
- if File.exists?(mail_room_config_file)
- puts "yes".green
- else
- puts "no".red
- try_fixing_it(
- "Copy config/mail_room.yml.example to config/mail_room.yml",
- "Check that the information in config/mail_room.yml is correct"
- )
- for_more_information(
- "doc/incoming_email/README.md"
- )
- fix_and_rerun
- end
- end
-
def check_imap_authentication
print "IMAP server credentials are correct? ... "
- mail_room_config_file = Rails.root.join("config", "mail_room.yml")
-
- unless File.exists?(mail_room_config_file)
- puts "can't check because of previous errors".magenta
- return
- end
-
- config = YAML.load_file(mail_room_config_file)[:mailboxes].first rescue nil
+ config = Gitlab.config.incoming_email
if config
begin
- imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl])
- imap.login(config[:email], config[:password])
+ imap = Net::IMAP.new(config.host, port: config.port, ssl: config.ssl)
+ imap.starttls if config.start_tls
+ imap.login(config.user, config.password)
connected = true
rescue
connected = false
@@ -791,7 +764,7 @@ namespace :gitlab do
else
puts "no".red
try_fixing_it(
- "Check that the information in config/mail_room.yml is correct"
+ "Check that the information in config/gitlab.yml is correct"
)
for_more_information(
"doc/incoming_email/README.md"
@@ -851,7 +824,7 @@ namespace :gitlab do
repo_dirs = Dir.glob(File.join(namespace_dir, '*'))
repo_dirs.each do |dir|
puts "\nChecking repo at #{dir}"
- system(*%w(git fsck), chdir: dir)
+ system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: dir)
end
end
end
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 6b1e3716147..9f5852ac613 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -46,43 +46,24 @@ namespace :gitlab do
desc "GitLab | Cleanup | Clean repositories"
task repos: :environment do
warn_user_is_not_gitlab
- remove_flag = ENV['REMOVE']
-
- git_base_path = Gitlab.config.gitlab_shell.repos_path
- all_dirs = Dir.glob(git_base_path + '/*')
-
- global_projects = Project.in_namespace(nil).pluck(:path)
-
- puts git_base_path.yellow
- puts "Looking for global repos to remove... "
-
- # skip non git repo
- all_dirs.select! do |dir|
- dir =~ /.git$/
- end
-
- # skip existing repos
- all_dirs.reject! do |dir|
- repo_name = File.basename dir
- path = repo_name.gsub(/\.git$/, "")
- global_projects.include?(path)
- end
- all_dirs.each do |dir_path|
- if remove_flag
- if FileUtils.rm_rf dir_path
- puts "Removed...#{dir_path}".red
- else
- puts "Cannot remove #{dir_path}".red
- end
- else
- puts "Can be removed: #{dir_path}".red
+ move_suffix = "+orphaned+#{Time.now.to_i}"
+ repo_root = Gitlab.config.gitlab_shell.repos_path
+ # Look for global repos (legacy, depth 1) and normal repos (depth 2)
+ IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find|
+ find.each_line do |path|
+ path.chomp!
+ repo_with_namespace = path.
+ sub(repo_root, '').
+ sub(%r{^/*}, '').
+ chomp('.git').
+ chomp('.wiki')
+ next if Project.find_with_namespace(repo_with_namespace)
+ new_path = path + move_suffix
+ puts path.inspect + ' -> ' + new_path.inspect
+ File.rename(path, new_path)
end
end
-
- unless remove_flag
- puts "To cleanup this directories run this command with REMOVE=true".yellow
- end
end
desc "GitLab | Cleanup | Block users that have been removed in LDAP"
diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake
index 0ac4b0fa8a3..4cbccf2ca89 100644
--- a/lib/tasks/gitlab/setup.rake
+++ b/lib/tasks/gitlab/setup.rake
@@ -16,6 +16,7 @@ namespace :gitlab do
Rake::Task["db:setup"].invoke
Rake::Task["add_limits_mysql"].invoke
+ Rake::Task["setup_postgresql"].invoke
Rake::Task["db:seed_fu"].invoke
rescue Gitlab::TaskAbortedByUserError
puts "Quitting...".red
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index 3c0cc763d17..dd61632e557 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -17,7 +17,7 @@ namespace :gitlab do
# Clone if needed
unless File.directory?(target_dir)
- system(*%W(git clone -- #{args.repo} #{target_dir}))
+ system(*%W(#{Gitlab.config.git.bin_path} clone -- #{args.repo} #{target_dir}))
end
# Make sure we're on the right tag
@@ -27,7 +27,7 @@ namespace :gitlab do
reseted = reset_to_commit(args)
unless reseted
- system(*%W(git fetch origin))
+ system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
reset_to_commit(args)
end
@@ -128,14 +128,14 @@ namespace :gitlab do
end
def reset_to_commit(args)
- tag, status = Gitlab::Popen.popen(%W(git describe -- #{args.tag}))
+ tag, status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} describe -- #{args.tag}))
unless status.zero?
- tag, status = Gitlab::Popen.popen(%W(git describe -- origin/#{args.tag}))
+ tag, status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} describe -- origin/#{args.tag}))
end
tag = tag.strip
- system(*%W(git reset --hard #{tag}))
+ system(*%W(#{Gitlab.config.git.bin_path} reset --hard #{tag}))
end
end
diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake
index c95b6540ebc..efb863a8764 100644
--- a/lib/tasks/gitlab/task_helpers.rake
+++ b/lib/tasks/gitlab/task_helpers.rake
@@ -2,16 +2,6 @@ module Gitlab
class TaskAbortedByUserError < StandardError; end
end
-unless STDOUT.isatty
- module Colored
- extend self
-
- def colorize(string, options={})
- string
- end
- end
-end
-
namespace :gitlab do
# Ask if the user wants to continue
@@ -103,7 +93,7 @@ namespace :gitlab do
gitlab_user = Gitlab.config.gitlab.user
current_user = run(%W(whoami)).chomp
unless current_user == gitlab_user
- puts "#{Colored.color(:black)+Colored.color(:on_yellow)} Warning #{Colored.extra(:clear)}"
+ puts " Warning ".colorize(:black).on_yellow
puts " You are running as user #{current_user.magenta}, we hope you know what you are doing."
puts " Things may work\/fail for the wrong reasons."
puts " For correct results you should run this as user #{gitlab_user.magenta}."
diff --git a/lib/tasks/gitlab/two_factor.rake b/lib/tasks/gitlab/two_factor.rake
new file mode 100644
index 00000000000..9196677a017
--- /dev/null
+++ b/lib/tasks/gitlab/two_factor.rake
@@ -0,0 +1,23 @@
+namespace :gitlab do
+ namespace :two_factor do
+ desc "GitLab | Disable Two-factor authentication (2FA) for all users"
+ task disable_for_all_users: :environment do
+ scope = User.with_two_factor
+ count = scope.count
+
+ if count > 0
+ puts "This will disable 2FA for #{count.to_s.red} users..."
+
+ begin
+ ask_to_continue
+ scope.find_each(&:disable_two_factor!)
+ puts "Successfully disabled 2FA for #{count} users.".green
+ rescue Gitlab::TaskAbortedByUserError
+ puts "Quitting...".red
+ end
+ else
+ puts "There are currently no users with 2FA enabled.".yellow
+ end
+ end
+ end
+end
diff --git a/lib/tasks/grape.rake b/lib/tasks/grape.rake
new file mode 100644
index 00000000000..9980e0b7984
--- /dev/null
+++ b/lib/tasks/grape.rake
@@ -0,0 +1,8 @@
+namespace :grape do
+ desc 'Print compiled grape routes'
+ task routes: :environment do
+ API::API.routes.each do |route|
+ puts route
+ end
+ end
+end
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
new file mode 100644
index 00000000000..141a0b74ec0
--- /dev/null
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -0,0 +1,8 @@
+require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes')
+require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes')
+
+desc 'GitLab | Sets up PostgreSQL'
+task setup_postgresql: :environment do
+ NamespacesProjectsPathLowerIndexes.new.up
+ AddUsersLowerUsernameEmailIndexes.new.up
+end
diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake
index 831746815d7..365ff2defd4 100644
--- a/lib/tasks/spec.rake
+++ b/lib/tasks/spec.rake
@@ -19,11 +19,20 @@ namespace :spec do
run_commands(cmds)
end
+ desc 'GitLab | Rspec | Run benchmark specs'
+ task :benchmark do
+ cmds = [
+ %W(rake gitlab:setup),
+ %W(rspec spec --tag @benchmark)
+ ]
+ run_commands(cmds)
+ end
+
desc 'GitLab | Rspec | Run other specs'
task :other do
cmds = [
%W(rake gitlab:setup),
- %W(rspec spec --tag ~@api --tag ~@feature)
+ %W(rspec spec --tag ~@api --tag ~@feature --tag ~@benchmark)
]
run_commands(cmds)
end
@@ -33,7 +42,7 @@ desc "GitLab | Run specs"
task :spec do
cmds = [
%W(rake gitlab:setup),
- %W(rspec spec),
+ %W(rspec spec --tag ~@benchmark),
]
run_commands(cmds)
end
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
index c8881be0954..d5a96fd38f4 100644
--- a/lib/tasks/spinach.rake
+++ b/lib/tasks/spinach.rake
@@ -5,7 +5,7 @@ namespace :spinach do
task :project do
cmds = [
%W(rake gitlab:setup),
- %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets),
+ %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets,~@commits),
]
run_commands(cmds)
end
@@ -14,7 +14,7 @@ namespace :spinach do
task :other do
cmds = [
%W(rake gitlab:setup),
- %W(spinach --tags @admin,@dashboard,@profile,@public,@snippets),
+ %W(spinach --tags @admin,@dashboard,@profile,@public,@snippets,@commits),
]
run_commands(cmds)
end
@@ -33,4 +33,4 @@ def run_commands(cmds)
cmds.each do |cmd|
system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!")
end
-end
+end \ No newline at end of file
diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb
new file mode 100644
index 00000000000..d4291f012d3
--- /dev/null
+++ b/lib/uploaded_file.rb
@@ -0,0 +1,37 @@
+require "tempfile"
+require "fileutils"
+
+# Taken from: Rack::Test::UploadedFile
+class UploadedFile
+
+ # The filename, *not* including the path, of the "uploaded" file
+ attr_reader :original_filename
+
+ # The tempfile
+ attr_reader :tempfile
+
+ # The content type of the "uploaded" file
+ attr_accessor :content_type
+
+ def initialize(path, filename, content_type = "text/plain")
+ raise "#{path} file does not exist" unless ::File.exist?(path)
+
+ @content_type = content_type
+ @original_filename = filename || ::File.basename(path)
+ @tempfile = File.new(path, 'rb')
+ end
+
+ def path
+ @tempfile.path
+ end
+
+ alias_method :local_path, :path
+
+ def method_missing(method_name, *args, &block) #:nodoc:
+ @tempfile.__send__(method_name, *args, &block)
+ end
+
+ def respond_to?(method_name, include_private = false) #:nodoc:
+ @tempfile.respond_to?(method_name, include_private) || super
+ end
+end
diff --git a/public/robots.txt b/public/robots.txt
index 528f421083e..4f616c7f4c1 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -38,7 +38,8 @@ Disallow: /*/*/edit
Disallow: /*/*/raw
Disallow: /*/*/blame
Disallow: /*/*/commits/*/*
-Disallow: /*/*/commit
+Disallow: /*/*/commit/*.patch
+Disallow: /*/*/commit/*.diff
Disallow: /*/*/compare
Disallow: /*/*/branches/new
Disallow: /*/*/tags/new
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index dca5e1c5db3..119cc90fc1e 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -1,7 +1,7 @@
#!/bin/bash
if [ -f /.dockerinit ]; then
- wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_1.9.0-1+b1_amd64.deb
- dpkg -i phantomjs_1.9.0-1+b1_amd64.deb
+ wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb
+ dpkg -i phantomjs_1.9.8-0jessie_amd64.deb
apt-get update -qq
apt-get install -y -qq libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client
diff --git a/public/uploads/.gitkeep b/shared/.gitkeep
index e69de29bb2d..e69de29bb2d 100644
--- a/public/uploads/.gitkeep
+++ b/shared/.gitkeep
diff --git a/tmp/.gitkeep b/shared/artifacts/.gitkeep
index e69de29bb2d..e69de29bb2d 100644
--- a/tmp/.gitkeep
+++ b/shared/artifacts/.gitkeep
diff --git a/shared/artifacts/tmp/cache/.gitkeep b/shared/artifacts/tmp/cache/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/shared/artifacts/tmp/cache/.gitkeep
diff --git a/shared/artifacts/tmp/uploads/.gitkeep b/shared/artifacts/tmp/uploads/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/shared/artifacts/tmp/uploads/.gitkeep
diff --git a/spec/benchmarks/finders/issues_finder_spec.rb b/spec/benchmarks/finders/issues_finder_spec.rb
new file mode 100644
index 00000000000..b57a33004a4
--- /dev/null
+++ b/spec/benchmarks/finders/issues_finder_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe IssuesFinder, benchmark: true do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ let(:label1) { create(:label, project: project, title: 'A') }
+ let(:label2) { create(:label, project: project, title: 'B') }
+
+ before do
+ 10.times do |n|
+ issue = create(:issue, author: user, project: project)
+
+ if n > 4
+ create(:label_link, label: label1, target: issue)
+ create(:label_link, label: label2, target: issue)
+ end
+ end
+ end
+
+ describe 'retrieving issues without labels' do
+ let(:finder) do
+ IssuesFinder.new(user, scope: 'all', label_name: Label::None.title,
+ state: 'opened')
+ end
+
+ benchmark_subject { finder.execute }
+
+ it { is_expected.to iterate_per_second(2000) }
+ end
+
+ describe 'retrieving issues with labels' do
+ let(:finder) do
+ IssuesFinder.new(user, scope: 'all', label_name: label1.title,
+ state: 'opened')
+ end
+
+ benchmark_subject { finder.execute }
+
+ it { is_expected.to iterate_per_second(1000) }
+ end
+
+ describe 'retrieving issues for a single project' do
+ let(:finder) do
+ IssuesFinder.new(user, scope: 'all', label_name: Label::None.title,
+ state: 'opened', project_id: project.id)
+ end
+
+ benchmark_subject { finder.execute }
+
+ it { is_expected.to iterate_per_second(2000) }
+ end
+ end
+end
diff --git a/spec/benchmarks/finders/trending_projects_finder_spec.rb b/spec/benchmarks/finders/trending_projects_finder_spec.rb
new file mode 100644
index 00000000000..551ce21840d
--- /dev/null
+++ b/spec/benchmarks/finders/trending_projects_finder_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe TrendingProjectsFinder, benchmark: true do
+ describe '#execute' do
+ let(:finder) { described_class.new }
+ let(:user) { create(:user) }
+
+ # to_a is used to force actually running the query (instead of just building
+ # it).
+ benchmark_subject { finder.execute(user).non_archived.to_a }
+
+ it { is_expected.to iterate_per_second(500) }
+ end
+end
diff --git a/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb b/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb
new file mode 100644
index 00000000000..34cd9f7e4eb
--- /dev/null
+++ b/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::Markdown::ReferenceFilter, benchmark: true do
+ let(:input) do
+ html = <<-EOF
+<p>Hello @alice and @bob, how are you doing today?</p>
+<p>This is simple @dummy text to see how the @ReferenceFilter class performs
+when @processing HTML.</p>
+ EOF
+
+ Nokogiri::HTML.fragment(html)
+ end
+
+ let(:project) { create(:empty_project) }
+
+ let(:filter) { described_class.new(input, project: project) }
+
+ describe '#replace_text_nodes_matching' do
+ let(:iterations) { 6000 }
+
+ describe 'with identical input and output HTML' do
+ benchmark_subject do
+ filter.replace_text_nodes_matching(User.reference_pattern) do |content|
+ content
+ end
+ end
+
+ it { is_expected.to iterate_per_second(iterations) }
+ end
+
+ describe 'with different input and output HTML' do
+ benchmark_subject do
+ filter.replace_text_nodes_matching(User.reference_pattern) do |content|
+ '@eve'
+ end
+ end
+
+ it { is_expected.to iterate_per_second(iterations) }
+ end
+ end
+end
diff --git a/spec/benchmarks/models/milestone_spec.rb b/spec/benchmarks/models/milestone_spec.rb
new file mode 100644
index 00000000000..a94afc4c40d
--- /dev/null
+++ b/spec/benchmarks/models/milestone_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Milestone, benchmark: true do
+ describe '#sort_issues' do
+ let(:milestone) { create(:milestone) }
+
+ let(:issue1) { create(:issue, milestone: milestone) }
+ let(:issue2) { create(:issue, milestone: milestone) }
+ let(:issue3) { create(:issue, milestone: milestone) }
+
+ let(:issue_ids) { [issue3.id, issue2.id, issue1.id] }
+
+ benchmark_subject { milestone.sort_issues(issue_ids) }
+
+ it { is_expected.to iterate_per_second(500) }
+ end
+end
diff --git a/spec/benchmarks/models/project_spec.rb b/spec/benchmarks/models/project_spec.rb
new file mode 100644
index 00000000000..cee0949edc5
--- /dev/null
+++ b/spec/benchmarks/models/project_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Project, benchmark: true do
+ describe '.trending' do
+ let(:group) { create(:group) }
+ let(:project1) { create(:empty_project, :public, group: group) }
+ let(:project2) { create(:empty_project, :public, group: group) }
+
+ let(:iterations) { 500 }
+
+ before do
+ 2.times do
+ create(:note_on_commit, project: project1)
+ end
+
+ create(:note_on_commit, project: project2)
+ end
+
+ describe 'without an explicit start date' do
+ benchmark_subject { described_class.trending.to_a }
+
+ it { is_expected.to iterate_per_second(iterations) }
+ end
+
+ describe 'with an explicit start date' do
+ let(:date) { 1.month.ago }
+
+ benchmark_subject { described_class.trending(date).to_a }
+
+ it { is_expected.to iterate_per_second(iterations) }
+ end
+ end
+
+ describe '.find_with_namespace' do
+ let(:group) { create(:group, name: 'sisinmaru') }
+ let(:project) { create(:project, name: 'maru', namespace: group) }
+
+ describe 'using a capitalized namespace' do
+ benchmark_subject { described_class.find_with_namespace('sisinmaru/MARU') }
+
+ it { is_expected.to iterate_per_second(600) }
+ end
+
+ describe 'using a lowercased namespace' do
+ benchmark_subject { described_class.find_with_namespace('sisinmaru/maru') }
+
+ it { is_expected.to iterate_per_second(600) }
+ end
+ end
+end
diff --git a/spec/benchmarks/models/project_team_spec.rb b/spec/benchmarks/models/project_team_spec.rb
new file mode 100644
index 00000000000..8b039ef7317
--- /dev/null
+++ b/spec/benchmarks/models/project_team_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe ProjectTeam, benchmark: true do
+ describe '#max_member_access' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, group: group) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+
+ 5.times do
+ project.team << [create(:user), :reporter]
+
+ project.group.add_user(create(:user), :reporter)
+ end
+ end
+
+ benchmark_subject { project.team.max_member_access(user.id) }
+
+ it { is_expected.to iterate_per_second(35000) }
+ end
+end
diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb
new file mode 100644
index 00000000000..1be7a8d3ed9
--- /dev/null
+++ b/spec/benchmarks/models/user_spec.rb
@@ -0,0 +1,78 @@
+require 'spec_helper'
+
+describe User, benchmark: true do
+ describe '.all' do
+ before do
+ 10.times { create(:user) }
+ end
+
+ benchmark_subject { User.all.to_a }
+
+ it { is_expected.to iterate_per_second(500) }
+ end
+
+ describe '.by_login' do
+ before do
+ %w{Alice Bob Eve}.each do |name|
+ create(:user,
+ email: "#{name}@gitlab.com",
+ username: name,
+ name: name)
+ end
+ end
+
+ # The iteration count is based on the query taking little over 1 ms when
+ # using PostgreSQL.
+ let(:iterations) { 900 }
+
+ describe 'using a capitalized username' do
+ benchmark_subject { User.by_login('Alice') }
+
+ it { is_expected.to iterate_per_second(iterations) }
+ end
+
+ describe 'using a lowercase username' do
+ benchmark_subject { User.by_login('alice') }
+
+ it { is_expected.to iterate_per_second(iterations) }
+ end
+
+ describe 'using a capitalized Email address' do
+ benchmark_subject { User.by_login('Alice@gitlab.com') }
+
+ it { is_expected.to iterate_per_second(iterations) }
+ end
+
+ describe 'using a lowercase Email address' do
+ benchmark_subject { User.by_login('alice@gitlab.com') }
+
+ it { is_expected.to iterate_per_second(iterations) }
+ end
+ end
+
+ describe '.find_by_any_email' do
+ let(:user) { create(:user) }
+
+ describe 'using a user with only a single Email address' do
+ let(:email) { user.email }
+
+ benchmark_subject { User.find_by_any_email(email) }
+
+ it { is_expected.to iterate_per_second(1000) }
+ end
+
+ describe 'using a user with multiple Email addresses' do
+ let(:email) { user.emails.first.email }
+
+ benchmark_subject { User.find_by_any_email(email) }
+
+ before do
+ 10.times do
+ user.emails.create(email: FFaker::Internet.email)
+ end
+ end
+
+ it { is_expected.to iterate_per_second(1000) }
+ end
+ end
+end
diff --git a/spec/benchmarks/services/projects/create_service_spec.rb b/spec/benchmarks/services/projects/create_service_spec.rb
new file mode 100644
index 00000000000..25ed48c34fd
--- /dev/null
+++ b/spec/benchmarks/services/projects/create_service_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Projects::CreateService, benchmark: true do
+ describe '#execute' do
+ let(:user) { create(:user, :admin) }
+
+ let(:group) do
+ group = create(:group)
+
+ create(:group_member, group: group, user: user)
+
+ group
+ end
+
+ benchmark_subject do
+ name = SecureRandom.hex
+ service = described_class.new(user,
+ name: name,
+ path: name,
+ namespace_id: group.id,
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+
+ service.execute
+ end
+
+ it { is_expected.to iterate_per_second(0.5) }
+ end
+end
diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb
new file mode 100644
index 00000000000..15824a1c67f
--- /dev/null
+++ b/spec/controllers/abuse_reports_controller_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+describe AbuseReportsController do
+ let(:reporter) { create(:user) }
+ let(:user) { create(:user) }
+ let(:message) { "This user is a spammer" }
+
+ before do
+ sign_in(reporter)
+ end
+
+ describe "POST create" do
+ context "with admin notification email set" do
+ let(:admin_email) { "admin@example.com"}
+
+ before(:each) do
+ stub_application_setting(admin_notification_email: admin_email)
+ end
+
+ it "sends a notification email" do
+ perform_enqueued_jobs do
+ post :create,
+ abuse_report: {
+ user_id: user.id,
+ message: message
+ }
+
+ email = ActionMailer::Base.deliveries.last
+
+ expect(email.to).to eq([admin_email])
+ expect(email.subject).to include(user.username)
+ expect(email.text_part.body).to include(message)
+ end
+ end
+
+ it "saves the abuse report" do
+ perform_enqueued_jobs do
+ expect do
+ post :create,
+ abuse_report: {
+ user_id: user.id,
+ message: message
+ }
+ end.to change { AbuseReport.count }.by(1)
+ end
+ end
+ end
+
+ context "without admin notification email set" do
+ before(:each) do
+ stub_application_setting(admin_notification_email: nil)
+ end
+
+ it "does not send a notification email" do
+ expect do
+ post :create,
+ abuse_report: {
+ user_id: user.id,
+ message: message
+ }
+ end.not_to change { ActionMailer::Base.deliveries.count }
+ end
+
+ it "saves the abuse report" do
+ expect do
+ post :create,
+ abuse_report: {
+ user_id: user.id,
+ message: message
+ }
+ end.to change { AbuseReport.count }.by(1)
+ end
+ end
+ end
+
+end
diff --git a/spec/controllers/admin/impersonation_controller_spec.rb b/spec/controllers/admin/impersonation_controller_spec.rb
new file mode 100644
index 00000000000..d7a7ba1c5b6
--- /dev/null
+++ b/spec/controllers/admin/impersonation_controller_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Admin::ImpersonationController do
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ describe 'CREATE #impersonation when blocked' do
+ let(:blocked_user) { create(:user, state: :blocked) }
+
+ it 'does not allow impersonation' do
+ post :create, id: blocked_user.username
+
+ expect(flash[:alert]).to eq 'You cannot impersonate a blocked user'
+ end
+ end
+end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index c40b2c2a583..8b7af4d3a0a 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -22,6 +22,32 @@ describe Admin::UsersController do
end
end
+ describe 'PUT block/:id' do
+ let(:user) { create(:user) }
+
+ it 'blocks user' do
+ put :block, id: user.username
+ user.reload
+ expect(user.blocked?).to be_truthy
+ expect(flash[:notice]).to eq 'Successfully blocked'
+ end
+ end
+
+ describe 'PUT unblock/:id' do
+ let(:user) { create(:user) }
+
+ before do
+ user.block
+ end
+
+ it 'unblocks user' do
+ put :unblock, id: user.username
+ user.reload
+ expect(user.blocked?).to be_falsey
+ expect(flash[:notice]).to eq 'Successfully unblocked'
+ end
+ end
+
describe 'PUT unlock/:id' do
let(:user) { create(:user) }
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index aa8d6cb807f..85379a8e984 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -114,7 +114,7 @@ describe AutocompleteController do
get(:users, project_id: project.id)
end
- it { expect(response.status).to eq(302) }
+ it { expect(response.status).to eq(404) }
end
describe 'GET #users with unknown project' do
@@ -122,7 +122,7 @@ describe AutocompleteController do
get(:users, project_id: 'unknown')
end
- it { expect(response.status).to eq(302) }
+ it { expect(response.status).to eq(404) }
end
describe 'GET #users with inaccessible group' do
@@ -131,7 +131,7 @@ describe AutocompleteController do
get(:users, group_id: user.namespace.id)
end
- it { expect(response.status).to eq(302) }
+ it { expect(response.status).to eq(404) }
end
describe 'GET #users with no project' do
@@ -139,7 +139,8 @@ describe AutocompleteController do
get(:users)
end
- it { expect(response.status).to eq(302) }
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq 0 }
end
end
end
diff --git a/spec/controllers/ci/commits_controller_spec.rb b/spec/controllers/ci/commits_controller_spec.rb
deleted file mode 100644
index b71e7505731..00000000000
--- a/spec/controllers/ci/commits_controller_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require "spec_helper"
-
-describe Ci::CommitsController do
- before do
- @project = FactoryGirl.create :ci_project
- end
-
- describe "GET /status" do
- it "returns status of commit" do
- commit = FactoryGirl.create :ci_commit, project: @project
- get :status, id: commit.sha, ref_id: commit.ref, project_id: @project.id
-
- expect(response).to be_success
- expect(response.code).to eq('200')
- JSON.parse(response.body)["status"] == "pending"
- end
-
- it "returns not_found status" do
- commit = FactoryGirl.create :ci_commit, project: @project
- get :status, id: commit.sha, ref_id: "deploy", project_id: @project.id
-
- expect(response).to be_success
- expect(response.code).to eq('200')
- JSON.parse(response.body)["status"] == "not_found"
- end
- end
-end
diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb
index bb3d87f3840..7793bf1e421 100644
--- a/spec/controllers/commit_controller_spec.rb
+++ b/spec/controllers/commit_controller_spec.rb
@@ -69,6 +69,21 @@ describe Projects::CommitController do
expect(response.body).to start_with("diff --git")
end
+
+ it "should really only be a git diff without whitespace changes" do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: '66eceea0db202bb39c4e445e8ca28689645366c5',
+ # id: commit.id,
+ format: format,
+ w: 1)
+
+ expect(response.body).to start_with("diff --git")
+ # without whitespace option, there are more than 2 diff_splits
+ diff_splits = assigns(:diffs)[0].diff.split("\n")
+ expect(diff_splits.length).to be <= 2
+ end
end
describe "as patch" do
@@ -95,6 +110,26 @@ describe Projects::CommitController do
expect(response.body).to match(/^diff --git/)
end
end
+
+ context 'commit that removes a submodule' do
+ render_views
+
+ let(:fork_project) { create(:forked_project_with_submodules) }
+ let(:commit) { fork_project.commit('remove-submodule') }
+
+ before do
+ fork_project.team << [user, :master]
+ end
+
+ it 'renders it' do
+ get(:show,
+ namespace_id: fork_project.namespace.to_param,
+ project_id: fork_project.to_param,
+ id: commit.id)
+
+ expect(response).to be_success
+ end
+ end
end
describe "#branches" do
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
new file mode 100644
index 00000000000..eb0c6ac6d80
--- /dev/null
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Groups::MilestonesController do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:project2) { create(:empty_project, group: group) }
+ let(:user) { create(:user) }
+ let(:title) { '肯定不是中文的问题' }
+
+ before do
+ sign_in(user)
+ group.add_owner(user)
+ project.team << [user, :master]
+ controller.instance_variable_set(:@group, group)
+ end
+
+ describe "#create" do
+ it "should create group milestone with Chinese title" do
+ post :create,
+ group_id: group.id,
+ milestone: { project_ids: [project.id, project2.id], title: title }
+
+ expect(response).to redirect_to(group_milestone_path(group, title.to_slug.to_s, title: title))
+ expect(Milestone.where(title: title).count).to eq(2)
+ end
+ end
+end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 766be578f7f..bbf8adef534 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -41,7 +41,7 @@ describe Import::GithubController do
it "assigns variables" do
@project = create(:project, import_type: 'github', creator_id: user.id)
- stub_client(repos: [@repo], orgs: [@org], org_repos: [@org_repo])
+ stub_client(repos: [@repo, @org_repo], orgs: [@org], org_repos: [@org_repo])
get :status
diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb
new file mode 100644
index 00000000000..3c6e54839b5
--- /dev/null
+++ b/spec/controllers/invites_controller_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe InvitesController do
+ let(:token) { '123456' }
+ let(:user) { create(:user) }
+ let(:member) { create(:project_member, invite_token: token, invite_email: 'test@abc.com', user: user) }
+
+ before do
+ controller.instance_variable_set(:@member, member)
+ sign_in(user)
+ end
+
+ describe 'GET #accept' do
+ it 'accepts user' do
+ get :accept, id: token
+ member.reload
+
+ expect(response.status).to eq(302)
+ expect(member.user).to eq(user)
+ expect(flash[:notice]).to include 'You have been granted'
+ end
+ end
+
+ describe 'GET #decline' do
+ it 'declines user' do
+ get :decline, id: token
+ expect{member.reload}.to raise_error ActiveRecord::RecordNotFound
+
+ expect(response.status).to eq(302)
+ expect(flash[:notice]).to include 'You have declined the invitation to join'
+ end
+ end
+end
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 2a447248b70..be19f1abc53 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -23,6 +23,22 @@ describe Projects::CompareController do
expect(assigns(:commits).length).to be >= 1
end
+ it 'compare should show some diffs with ignore whitespace change option' do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ from: '08f22f25',
+ to: '66eceea0',
+ w: 1)
+
+ expect(response).to be_success
+ expect(assigns(:diffs).length).to be >= 1
+ expect(assigns(:commits).length).to be >= 1
+ # without whitespace option, there are more than 2 diff_splits
+ diff_splits = assigns(:diffs)[0].diff.split("\n")
+ expect(diff_splits.length).to be <= 2
+ end
+
describe 'non-existent refs' do
it 'invalid source ref' do
get(:show,
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 871b9219ca9..76d56bc989d 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -8,28 +8,34 @@ describe Projects::IssuesController do
before do
sign_in(user)
project.team << [user, :developer]
- controller.instance_variable_set(:@project, project)
end
describe "GET #index" do
it "returns index" do
- get :index, namespace_id: project.namespace.id, project_id: project.id
+ get :index, namespace_id: project.namespace.path, project_id: project.path
expect(response.status).to eq(200)
end
+ it "return 301 if request path doesn't match project path" do
+ get :index, namespace_id: project.namespace.path, project_id: project.path.upcase
+
+ expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project))
+ end
+
it "returns 404 when issues are disabled" do
project.issues_enabled = false
project.save
- get :index, namespace_id: project.namespace.id, project_id: project.id
+ get :index, namespace_id: project.namespace.path, project_id: project.path
expect(response.status).to eq(404)
end
it "returns 404 when external issue tracker is enabled" do
+ controller.instance_variable_set(:@project, project)
allow(project).to receive(:default_issues_tracker?).and_return(false)
- get :index, namespace_id: project.namespace.id, project_id: project.id
+ get :index, namespace_id: project.namespace.path, project_id: project.path
expect(response.status).to eq(404)
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index b8db8591709..6aaec224f6e 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -10,6 +10,30 @@ describe Projects::MergeRequestsController do
project.team << [user, :master]
end
+ describe '#new' do
+ context 'merge request that removes a submodule' do
+ render_views
+
+ let(:fork_project) { create(:forked_project_with_submodules) }
+
+ before do
+ fork_project.team << [user, :master]
+ end
+
+ it 'renders it' do
+ get :new,
+ namespace_id: fork_project.namespace.to_param,
+ project_id: fork_project.to_param,
+ merge_request: {
+ source_branch: 'remove-submodule',
+ target_branch: 'master'
+ }
+
+ expect(response).to be_success
+ end
+ end
+ end
+
describe "#show" do
shared_examples "export merge as" do |format|
it "should generally work" do
@@ -147,6 +171,34 @@ describe Projects::MergeRequestsController do
end
end
+ describe 'GET diffs with ignore_whitespace_change' do
+ def go(format: 'html')
+ get :diffs,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid,
+ format: format,
+ w: 1
+ end
+
+ context 'as html' do
+ it 'renders the diff template' do
+ go
+
+ expect(response).to render_template('diffs')
+ end
+ end
+
+ context 'as json' do
+ it 'renders the diffs template to a string' do
+ go format: 'json'
+
+ expect(response).to render_template('projects/merge_requests/show/_diffs')
+ expect(JSON.parse(response.body)).to have_key('html')
+ end
+ end
+ end
+
describe 'GET commits' do
def go(format: 'html')
get :commits,
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 8127efabe6e..d173bb350f1 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::MilestonesController do
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
let(:issue) { create(:issue, project: project, milestone: milestone) }
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
before do
sign_in(user)
@@ -15,10 +15,9 @@ describe Projects::MilestonesController do
describe "#destroy" do
it "should remove milestone" do
- merge_request.reload
expect(issue.milestone_id).to eq(milestone.id)
- delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.id, format: :js
+ delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js
expect(response).to be_success
expect(Event.first.action).to eq(Event::DESTROYED)
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 91856ed0cc0..18a30033ed8 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -33,33 +33,5 @@ describe Projects::RepositoriesController do
expect(response.status).to eq(404)
end
end
-
- context "when the service doesn't return a path" do
-
- before do
- allow(service).to receive(:execute).and_return(nil)
- end
-
- it "reloads the page" do
- get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
-
- expect(response).to redirect_to(archive_namespace_project_repository_path(project.namespace, project, ref: "master", format: "zip"))
- end
- end
-
- context "when the service returns a path" do
-
- let(:path) { Rails.root.join("spec/fixtures/dk.png").to_s }
-
- before do
- allow(service).to receive(:execute).and_return(path)
- end
-
- it "sends the file" do
- get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
-
- expect(response.body).to eq(File.binread(path))
- end
- end
end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index d4ecd98e12d..ccd8c741c83 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -10,26 +10,43 @@ describe Projects::ServicesController do
project.team << [user, :master]
controller.instance_variable_set(:@project, project)
controller.instance_variable_set(:@service, service)
- request.env["HTTP_REFERER"] = "/"
end
- describe "#test" do
- context 'success' do
- it "should redirect and show success message" do
- expect(service).to receive(:test).and_return({ success: true, result: 'done' })
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
- expect(response.status).to redirect_to('/')
- expect(flash[:notice]).to eq('We sent a request to the provided URL')
- end
+ shared_examples_for 'services controller' do |referrer|
+ before do
+ request.env["HTTP_REFERER"] = referrer
end
- context 'failure' do
- it "should redirect and show failure message" do
- expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' })
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
- expect(response.status).to redirect_to('/')
- expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test')
+ describe "#test" do
+ context 'success' do
+ it "should redirect and show success message" do
+ expect(service).to receive(:test).and_return({ success: true, result: 'done' })
+ get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ expect(response.status).to redirect_to('/')
+ expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ end
+ end
+
+ context 'failure' do
+ it "should redirect and show failure message" do
+ expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' })
+ get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ expect(response.status).to redirect_to('/')
+ expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test')
+ end
end
end
end
+
+ describe 'referrer defined' do
+ it_should_behave_like 'services controller' do
+ let!(:referrer) { "/" }
+ end
+ end
+
+ describe 'referrer undefined' do
+ it_should_behave_like 'services controller' do
+ let!(:referrer) { nil }
+ end
+ end
end
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index 53915856357..a474574c6e5 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -88,4 +88,40 @@ describe Projects::TreeController do
end
end
end
+
+ describe '#create_dir' do
+ render_views
+
+ before do
+ post(:create_dir,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: 'master',
+ dir_name: path,
+ new_branch: target_branch,
+ commit_message: 'Test commit message')
+ end
+
+ context 'successful creation' do
+ let(:path) { 'files/new_dir'}
+ let(:target_branch) { 'master-test'}
+
+ it 'redirects to the new directory' do
+ expect(subject).
+ to redirect_to("/#{project.path_with_namespace}/blob/#{target_branch}/#{path}")
+ expect(flash[:notice]).to eq('The directory has been successfully created')
+ end
+ end
+
+ context 'unsuccessful creation' do
+ let(:path) { 'README.md' }
+ let(:target_branch) { 'master'}
+
+ it 'does not allow overwriting of existing files' do
+ expect(subject).
+ to redirect_to("/#{project.path_with_namespace}/blob/master")
+ expect(flash[:alert]).to eq('Directory already exists as a file')
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index f51abfedae5..93c4494c660 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -33,7 +33,7 @@ describe Projects::UploadsController do
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"rails_sample\"'
- expect(response.body).to match "\"url\":\"http://localhost/#{project.path_with_namespace}/uploads"
+ expect(response.body).to match "\"url\":\"/uploads"
expect(response.body).to match '\"is_image\":true'
end
end
@@ -49,7 +49,7 @@ describe Projects::UploadsController do
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
- expect(response.body).to match "\"url\":\"http://localhost/#{project.path_with_namespace}/uploads"
+ expect(response.body).to match "\"url\":\"/uploads"
expect(response.body).to match '\"is_image\":false'
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 29233e9fae6..665526fde93 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -21,6 +21,87 @@ describe ProjectsController do
expect(response.body).to include("content='#{content}'")
end
end
+
+ context "rendering default project view" do
+ render_views
+
+ it "renders the activity view" do
+ allow(controller).to receive(:current_user).and_return(user)
+ allow(user).to receive(:project_view).and_return('activity')
+
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ expect(response).to render_template('_activity')
+ end
+
+ it "renders the readme view" do
+ allow(controller).to receive(:current_user).and_return(user)
+ allow(user).to receive(:project_view).and_return('readme')
+
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ expect(response).to render_template('_readme')
+ end
+
+ it "renders the files view" do
+ allow(controller).to receive(:current_user).and_return(user)
+ allow(user).to receive(:project_view).and_return('files')
+
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ expect(response).to render_template('_files')
+ end
+ end
+
+ context "when requested with case sensitive namespace and project path" do
+ context "when there is a match with the same casing" do
+ it "loads the project" do
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
+
+ expect(assigns(:project)).to eq(public_project)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context "when there is a match with different casing" do
+ it "redirects to the normalized path" do
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
+
+ expect(assigns(:project)).to eq(public_project)
+ expect(response).to redirect_to("/#{public_project.path_with_namespace}")
+ end
+
+
+ # MySQL queries are case insensitive by default, so this spec would fail.
+ if Gitlab::Database.postgresql?
+ context "when there is also a match with the same casing" do
+
+ let!(:other_project) { create(:project, :public, namespace: public_project.namespace, path: public_project.path.upcase) }
+
+ it "loads the exactly matched project" do
+
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
+
+ expect(assigns(:project)).to eq(other_project)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe "#destroy" do
+ let(:admin) { create(:admin) }
+
+ it "redirects to the dashboard" do
+ controller.instance_variable_set(:@project, project)
+ sign_in(admin)
+
+ orig_id = project.id
+ delete :destroy, namespace_id: project.namespace.path, id: project.path
+
+ expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(response.status).to eq(302)
+ expect(response).to redirect_to(dashboard_projects_path)
+ end
end
describe "POST #toggle_star" do
@@ -48,4 +129,50 @@ describe ProjectsController do
expect(user.starred?(public_project)).to be_falsey
end
end
+
+ describe "DELETE remove_fork" do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'with forked project' do
+ let(:project_fork) { create(:project, namespace: user.namespace) }
+
+ before do
+ create(:forked_project_link, forked_to_project: project_fork)
+ end
+
+ it 'should remove fork from project' do
+ delete(:remove_fork,
+ namespace_id: project_fork.namespace.to_param,
+ id: project_fork.to_param, format: :js)
+
+ expect(project_fork.forked?).to be_falsey
+ expect(flash[:notice]).to eq('The fork relationship has been removed.')
+ expect(response).to render_template(:remove_fork)
+ end
+ end
+
+ context 'when project not forked' do
+ let(:unforked_project) { create(:project, namespace: user.namespace) }
+
+ it 'should do nothing if project was not forked' do
+ delete(:remove_fork,
+ namespace_id: unforked_project.namespace.to_param,
+ id: unforked_project.to_param, format: :js)
+
+ expect(flash[:notice]).to be_nil
+ expect(response).to render_template(:remove_fork)
+ end
+ end
+ end
+
+ it "does nothing if user is not signed in" do
+ delete(:remove_fork,
+ namespace_id: project.namespace.to_param,
+ id: project.to_param, format: :js)
+ expect(response.status).to eq(401)
+ end
+ end
end
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index 64dfe8f34e3..5a104ae7c99 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -10,7 +10,7 @@ describe RootController do
allow(subject).to receive(:current_user).and_return(user)
end
- context 'who has customized their dashboard setting' do
+ context 'who has customized their dashboard setting for starred projects' do
before do
user.update_attribute(:dashboard, 'stars')
end
@@ -21,6 +21,28 @@ describe RootController do
end
end
+ context 'who has customized their dashboard setting for project activities' do
+ before do
+ user.update_attribute(:dashboard, 'project_activity')
+ end
+
+ it 'redirects to the activity list' do
+ get :index
+ expect(response).to redirect_to activity_dashboard_path
+ end
+ end
+
+ context 'who has customized their dashboard setting for starred project activities' do
+ before do
+ user.update_attribute(:dashboard, 'starred_project_activity')
+ end
+
+ it 'redirects to the activity list' do
+ get :index
+ expect(response).to redirect_to activity_dashboard_path(filter: 'starred')
+ end
+ end
+
context 'who uses the default dashboard setting' do
it 'renders the default dashboard' do
get :index
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
new file mode 100644
index 00000000000..b3dcb52c500
--- /dev/null
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -0,0 +1,233 @@
+require 'spec_helper'
+
+describe SnippetsController do
+ describe 'GET #show' do
+ let(:user) { create(:user) }
+
+ context 'when the personal snippet is private' do
+ let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when signed in user is not the author' do
+ let(:other_author) { create(:author) }
+ let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
+
+ it 'responds with status 404' do
+ get :show, id: other_personal_snippet.to_param
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when signed in user is the author' do
+ it 'renders the snippet' do
+ get :show, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :show, id: personal_snippet.to_param
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context 'when the personal snippet is internal' do
+ let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'renders the snippet' do
+ get :show, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :show, id: personal_snippet.to_param
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context 'when the personal snippet is public' do
+ let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'renders the snippet' do
+ get :show, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'renders the snippet' do
+ get :show, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context 'when the personal snippet does not exist' do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'responds with status 404' do
+ get :show, id: 'doesntexist'
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'responds with status 404' do
+ get :show, id: 'doesntexist'
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+
+ describe 'GET #raw' do
+ let(:user) { create(:user) }
+
+ context 'when the personal snippet is private' do
+ let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when signed in user is not the author' do
+ let(:other_author) { create(:author) }
+ let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
+
+ it 'responds with status 404' do
+ get :raw, id: other_personal_snippet.to_param
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when signed in user is the author' do
+ it 'renders the raw snippet' do
+ get :raw, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :raw, id: personal_snippet.to_param
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context 'when the personal snippet is internal' do
+ let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'renders the raw snippet' do
+ get :raw, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :raw, id: personal_snippet.to_param
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context 'when the personal snippet is public' do
+ let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'renders the raw snippet' do
+ get :raw, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'renders the raw snippet' do
+ get :raw, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context 'when the personal snippet does not exist' do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'responds with status 404' do
+ get :raw, id: 'doesntexist'
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'responds with status 404' do
+ get :raw, id: 'doesntexist'
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 9f89101d7f7..104a5f50143 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -16,13 +16,26 @@ describe UsersController do
context 'with rendered views' do
render_views
- it 'renders the show template' do
- sign_in(user)
+ describe 'when logged in' do
+ before do
+ sign_in(user)
+ end
- get :show, username: user.username
+ it 'renders the show template' do
+ get :show, username: user.username
- expect(response).to be_success
- expect(response).to render_template('show')
+ expect(response).to be_success
+ expect(response).to render_template('show')
+ end
+ end
+
+ describe 'when logged out' do
+ it 'renders the show template' do
+ get :show, username: user.username
+
+ expect(response).to be_success
+ expect(response).to render_template('show')
+ end
end
end
end
diff --git a/spec/factories.rb b/spec/factories.rb
index 200f18f660d..4bf93adabe2 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -165,6 +165,18 @@ FactoryGirl.define do
title
content
file_name
+
+ trait :public do
+ visibility_level Gitlab::VisibilityLevel::PUBLIC
+ end
+
+ trait :internal do
+ visibility_level Gitlab::VisibilityLevel::INTERNAL
+ end
+
+ trait :private do
+ visibility_level Gitlab::VisibilityLevel::PRIVATE
+ end
end
factory :snippet do
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 99da5a18776..2fcd70182b9 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -27,6 +27,9 @@
FactoryGirl.define do
factory :ci_build, class: Ci::Build do
+ name 'test'
+ ref 'master'
+ tag false
started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013'
commands 'ls -a'
@@ -43,5 +46,9 @@ FactoryGirl.define do
started_at nil
finished_at nil
end
+
+ factory :ci_build_tag do
+ tag true
+ end
end
end
diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb
index 70930c789c3..79e000b7ccb 100644
--- a/spec/factories/ci/commits.rb
+++ b/spec/factories/ci/commits.rb
@@ -17,58 +17,32 @@
# Read about factories at https://github.com/thoughtbot/factory_girl
FactoryGirl.define do
- factory :ci_commit, class: Ci::Commit do
- ref 'master'
- before_sha '76de212e80737a608d939f648d959671fb0a0142'
+ factory :ci_empty_commit, class: Ci::Commit do
sha '97de212e80737a608d939f648d959671fb0a0142'
- push_data do
- {
- ref: 'refs/heads/master',
- before: '76de212e80737a608d939f648d959671fb0a0142',
- after: '97de212e80737a608d939f648d959671fb0a0142',
- user_name: 'Git User',
- user_email: 'git@example.com',
- repository: {
- name: 'test-data',
- url: 'ssh://git@gitlab.com/test/test-data.git',
- description: '',
- homepage: 'http://gitlab.com/test/test-data'
- },
- commits: [
- {
- id: '97de212e80737a608d939f648d959671fb0a0142',
- message: 'Test commit message',
- timestamp: '2014-09-23T13:12:25+02:00',
- url: 'https://gitlab.com/test/test-data/commit/97de212e80737a608d939f648d959671fb0a0142',
- author: {
- name: 'Git User',
- email: 'git@user.com'
- }
- }
- ],
- total_commits_count: 1,
- ci_yaml_file: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
- }
- end
+
+ gl_project factory: :empty_project
factory :ci_commit_without_jobs do
- after(:create) do |commit, evaluator|
- commit.push_data[:ci_yaml_file] = YAML.dump({})
- commit.save
+ after(:build) do |commit|
+ allow(commit).to receive(:ci_yaml_file) { YAML.dump({}) }
end
end
factory :ci_commit_with_one_job do
- after(:create) do |commit, evaluator|
- commit.push_data[:ci_yaml_file] = YAML.dump({ rspec: { script: "ls" } })
- commit.save
+ after(:build) do |commit|
+ allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) }
end
end
factory :ci_commit_with_two_jobs do
- after(:create) do |commit, evaluator|
- commit.push_data[:ci_yaml_file] = YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } })
- commit.save
+ after(:build) do |commit|
+ allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) }
+ end
+ end
+
+ factory :ci_commit do
+ after(:build) do |commit|
+ allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
end
end
end
diff --git a/spec/factories/ci/projects.rb b/spec/factories/ci/projects.rb
index e6bd0685f8d..11cb8c9eeaa 100644
--- a/spec/factories/ci/projects.rb
+++ b/spec/factories/ci/projects.rb
@@ -29,28 +29,22 @@
FactoryGirl.define do
factory :ci_project_without_token, class: Ci::Project do
- sequence :name do |n|
- "GitLab / gitlab-shell#{n}"
- end
-
default_ref 'master'
- sequence :path do |n|
- "gitlab/gitlab-shell#{n}"
- end
-
- sequence :ssh_url_to_repo do |n|
- "git@demo.gitlab.com:gitlab/gitlab-shell#{n}.git"
- end
-
- gl_project factory: :project
+ shared_runners_enabled false
factory :ci_project do
token 'iPWx6WM4lhHNedGfBpPJNP'
end
- factory :ci_public_project do
- public true
+ initialize_with do
+ # TODO:
+ # this is required, because builds_enabled is initialized when Project is created
+ # and this create gitlab_ci_project if builds is set to true
+ # here we take created gitlab_ci_project and update it's attributes
+ ci_project = create(:empty_project).ensure_gitlab_ci_project
+ ci_project.update_attributes(attributes)
+ ci_project
end
end
end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
new file mode 100644
index 00000000000..52de437052d
--- /dev/null
+++ b/spec/factories/commit_statuses.rb
@@ -0,0 +1,15 @@
+FactoryGirl.define do
+ factory :commit_status, class: CommitStatus do
+ started_at 'Di 29. Okt 09:51:28 CET 2013'
+ finished_at 'Di 29. Okt 09:53:28 CET 2013'
+ name 'default'
+ status 'success'
+ description 'commit status'
+ commit factory: :ci_commit
+
+ factory :generic_commit_status, class: GenericCommitStatus do
+ name 'generic'
+ description 'external commit status'
+ end
+ end
+end
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
index 6829387c660..8b12ee11af5 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -8,6 +8,7 @@
# project_id :integer
# created_at :datetime
# updated_at :datetime
+# template :boolean default(FALSE)
#
# Read about factories at https://github.com/thoughtbot/factory_girl
diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb
new file mode 100644
index 00000000000..7fb2d77ca32
--- /dev/null
+++ b/spec/factories/lfs_objects.rb
@@ -0,0 +1,12 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :lfs_object do
+ oid "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80"
+ size 499013
+ end
+
+ trait :with_file do
+ file { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") }
+ end
+end
diff --git a/spec/factories/lfs_objects_projects.rb b/spec/factories/lfs_objects_projects.rb
new file mode 100644
index 00000000000..93de6607df8
--- /dev/null
+++ b/spec/factories/lfs_objects_projects.rb
@@ -0,0 +1,8 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :lfs_objects_project do
+ lfs_object
+ project
+ end
+end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 6080d0ccdef..729a49c9f72 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -20,6 +20,7 @@
# position :integer default(0)
# locked_at :datetime
# updated_by_id :integer
+# merge_error :string(255)
#
FactoryGirl.define do
diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb
new file mode 100644
index 00000000000..43d09b17534
--- /dev/null
+++ b/spec/factories/releases.rb
@@ -0,0 +1,21 @@
+# == Schema Information
+#
+# Table name: releases
+#
+# id :integer not null, primary key
+# tag :string(255)
+# description :text
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :release do
+ tag "v1.1.0"
+ description "Awesome release"
+ project
+ end
+end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 86717761582..4570e409128 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -87,13 +87,16 @@ describe "Admin::Users", feature: true do
end
it "should call send mail" do
- expect(Notify).to receive(:new_user_email)
+ expect_any_instance_of(NotificationService).to receive(:new_user)
click_button "Create user"
end
it "should send valid email to user with email & password" do
- click_button "Create user"
+ perform_enqueued_jobs do
+ click_button "Create user"
+ end
+
user = User.find_by(username: 'bang')
email = ActionMailer::Base.deliveries.last
expect(email.subject).to have_content('Account was created')
@@ -111,6 +114,63 @@ describe "Admin::Users", feature: true do
expect(page).to have_content(@user.name)
end
+ describe 'Impersonation' do
+ let(:another_user) { create(:user) }
+ before { visit admin_user_path(another_user) }
+
+ context 'before impersonating' do
+ it 'shows impersonate button for other users' do
+ expect(page).to have_content('Impersonate')
+ end
+
+ it 'should not show impersonate button for admin itself' do
+ visit admin_user_path(@user)
+
+ expect(page).not_to have_content('Impersonate')
+ end
+
+ it 'should not show impersonate button for blocked user' do
+ another_user.block
+
+ visit admin_user_path(another_user)
+
+ expect(page).not_to have_content('Impersonate')
+
+ another_user.activate
+ end
+ end
+
+ context 'when impersonating' do
+ before { click_link 'Impersonate' }
+
+ it 'logs in as the user when impersonate is clicked' do
+ page.within '.sidebar-user .username' do
+ expect(page).to have_content(another_user.username)
+ end
+ end
+
+ it 'sees impersonation log out icon' do
+ icon = first('.fa.fa-user-secret')
+
+ expect(icon).to_not eql nil
+ end
+
+ it 'can log out of impersonated user back to original user' do
+ find(:css, 'li.impersonation a').click
+
+ page.within '.sidebar-user .username' do
+ expect(page).to have_content(@user.username)
+ end
+ end
+
+ it 'is redirected back to the impersonated users page in the admin after stopping' do
+ find(:css, 'li.impersonation a').click
+
+ expect(current_path).to eql "/admin/users/#{another_user.username}"
+ end
+ end
+ end
+
describe 'Two-factor Authentication status' do
it 'shows when enabled' do
@user.update_attribute(:two_factor_enabled, true)
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
new file mode 100644
index 00000000000..5213ce1099f
--- /dev/null
+++ b/spec/features/builds_spec.rb
@@ -0,0 +1,114 @@
+require 'spec_helper'
+
+describe "Builds" do
+ let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
+
+ before do
+ login_as(:user)
+ @commit = FactoryGirl.create :ci_commit
+ @build = FactoryGirl.create :ci_build, commit: @commit
+ @gl_project = @commit.project.gl_project
+ @gl_project.team << [@user, :master]
+ end
+
+ describe "GET /:project/builds" do
+ context "Running scope" do
+ before do
+ @build.run!
+ visit namespace_project_builds_path(@gl_project.namespace, @gl_project)
+ end
+
+ it { expect(page).to have_content 'Running' }
+ it { expect(page).to have_content 'Cancel all' }
+ it { expect(page).to have_content @build.short_sha }
+ it { expect(page).to have_content @build.ref }
+ it { expect(page).to have_content @build.name }
+ end
+
+ context "Finished scope" do
+ before do
+ @build.run!
+ visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :finished)
+ end
+
+ it { expect(page).to have_content 'No builds to show' }
+ it { expect(page).to have_content 'Cancel all' }
+ end
+
+ context "All builds" do
+ before do
+ @gl_project.ci_builds.running_or_pending.each(&:success)
+ visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :all)
+ end
+
+ it { expect(page).to have_content 'All' }
+ it { expect(page).to have_content @build.short_sha }
+ it { expect(page).to have_content @build.ref }
+ it { expect(page).to have_content @build.name }
+ it { expect(page).to_not have_content 'Cancel all' }
+ end
+ end
+
+ describe "POST /:project/builds/:id/cancel_all" do
+ before do
+ @build.run!
+ visit namespace_project_builds_path(@gl_project.namespace, @gl_project)
+ click_link "Cancel all"
+ end
+
+ it { expect(page).to have_content 'No builds to show' }
+ it { expect(page).to_not have_content 'Cancel all' }
+ end
+
+ describe "GET /:project/builds/:id" do
+ before do
+ visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build)
+ end
+
+ it { expect(page).to have_content @commit.sha[0..7] }
+ it { expect(page).to have_content @commit.git_commit_message }
+ it { expect(page).to have_content @commit.git_author_name }
+
+ context "Download artifacts" do
+ before do
+ @build.update_attributes(artifacts_file: artifacts_file)
+ visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build)
+ end
+
+ it { expect(page).to have_content 'Download artifacts' }
+ end
+ end
+
+ describe "POST /:project/builds/:id/cancel" do
+ before do
+ @build.run!
+ visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build)
+ click_link "Cancel"
+ end
+
+ it { expect(page).to have_content 'canceled' }
+ it { expect(page).to have_content 'Retry' }
+ end
+
+ describe "POST /:project/builds/:id/retry" do
+ before do
+ @build.run!
+ visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build)
+ click_link "Cancel"
+ click_link 'Retry'
+ end
+
+ it { expect(page).to have_content 'pending' }
+ it { expect(page).to have_content 'Cancel' }
+ end
+
+ describe "GET /:project/builds/:id/download" do
+ before do
+ @build.update_attributes(artifacts_file: artifacts_file)
+ visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build)
+ click_link 'Download artifacts'
+ end
+
+ it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
+ end
+end
diff --git a/spec/features/ci/admin/builds_spec.rb b/spec/features/ci/admin/builds_spec.rb
index 88ef9c144af..623d466c67b 100644
--- a/spec/features/ci/admin/builds_spec.rb
+++ b/spec/features/ci/admin/builds_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
describe "Admin Builds" do
- let(:project) { FactoryGirl.create :ci_project }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:commit) { FactoryGirl.create :ci_commit }
let(:build) { FactoryGirl.create :ci_build, commit: commit }
before do
@@ -22,10 +21,10 @@ describe "Admin Builds" do
describe "Tabs" do
it "shows all builds" do
- build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
- build1 = FactoryGirl.create :ci_build, commit: commit, status: "running"
- build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
- build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed"
+ FactoryGirl.create :ci_build, commit: commit, status: "pending"
+ FactoryGirl.create :ci_build, commit: commit, status: "running"
+ FactoryGirl.create :ci_build, commit: commit, status: "success"
+ FactoryGirl.create :ci_build, commit: commit, status: "failed"
visit ci_admin_builds_path
diff --git a/spec/features/ci/admin/runners_spec.rb b/spec/features/ci/admin/runners_spec.rb
index b25121f0806..b83744f53a8 100644
--- a/spec/features/ci/admin/runners_spec.rb
+++ b/spec/features/ci/admin/runners_spec.rb
@@ -2,8 +2,7 @@ require 'spec_helper'
describe "Admin Runners" do
before do
- skip_ci_admin_auth
- login_as :user
+ login_as :admin
end
describe "Runners page" do
@@ -20,16 +19,16 @@ describe "Admin Runners" do
describe 'search' do
before do
- FactoryGirl.create :ci_runner, description: 'foo'
- FactoryGirl.create :ci_runner, description: 'bar'
+ FactoryGirl.create :ci_runner, description: 'runner-foo'
+ FactoryGirl.create :ci_runner, description: 'runner-bar'
search_form = find('#runners-search')
- search_form.fill_in 'search', with: 'foo'
+ search_form.fill_in 'search', with: 'runner-foo'
search_form.click_button 'Search'
end
- it { expect(page).to have_content("foo") }
- it { expect(page).not_to have_content("bar") }
+ it { expect(page).to have_content("runner-foo") }
+ it { expect(page).not_to have_content("runner-bar") }
end
end
@@ -37,8 +36,8 @@ describe "Admin Runners" do
let(:runner) { FactoryGirl.create :ci_runner }
before do
- FactoryGirl.create(:ci_project, name: "foo")
- FactoryGirl.create(:ci_project, name: "bar")
+ @project1 = FactoryGirl.create(:ci_project)
+ @project2 = FactoryGirl.create(:ci_project)
visit ci_admin_runner_path(runner)
end
@@ -47,19 +46,19 @@ describe "Admin Runners" do
end
describe 'projects' do
- it { expect(page).to have_content("foo") }
- it { expect(page).to have_content("bar") }
+ it { expect(page).to have_content(@project1.name_with_namespace) }
+ it { expect(page).to have_content(@project2.name_with_namespace) }
end
describe 'search' do
before do
search_form = find('#runner-projects-search')
- search_form.fill_in 'search', with: 'foo'
+ search_form.fill_in 'search', with: @project1.gl_project.name
search_form.click_button 'Search'
end
- it { expect(page).to have_content("foo") }
- it { expect(page).not_to have_content("bar") }
+ it { expect(page).to have_content(@project1.name_with_namespace) }
+ it { expect(page).not_to have_content(@project2.name_with_namespace) }
end
end
end
diff --git a/spec/features/ci/builds_spec.rb b/spec/features/ci/builds_spec.rb
deleted file mode 100644
index 2f020e524e2..00000000000
--- a/spec/features/ci/builds_spec.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-require 'spec_helper'
-
-describe "Builds" do
- context :private_project do
- before do
- @project = FactoryGirl.create :ci_project
- @commit = FactoryGirl.create :ci_commit, project: @project
- @build = FactoryGirl.create :ci_build, commit: @commit
- login_as :user
- @project.gl_project.team << [@user, :master]
- end
-
- describe "GET /:project/builds/:id" do
- before do
- visit ci_project_build_path(@project, @build)
- end
-
- it { expect(page).to have_content @commit.sha[0..7] }
- it { expect(page).to have_content @commit.git_commit_message }
- it { expect(page).to have_content @commit.git_author_name }
- end
-
- describe "GET /:project/builds/:id/cancel" do
- before do
- @build.run!
- visit cancel_ci_project_build_path(@project, @build)
- end
-
- it { expect(page).to have_content 'canceled' }
- it { expect(page).to have_content 'Retry' }
- end
-
- describe "POST /:project/builds/:id/retry" do
- before do
- @build.cancel!
- visit ci_project_build_path(@project, @build)
- click_link 'Retry'
- end
-
- it { expect(page).to have_content 'pending' }
- it { expect(page).to have_content 'Cancel' }
- end
- end
-
- context :public_project do
- describe "Show page public accessible" do
- before do
- @project = FactoryGirl.create :ci_public_project
- @commit = FactoryGirl.create :ci_commit, project: @project
- @runner = FactoryGirl.create :ci_specific_runner
- @build = FactoryGirl.create :ci_build, commit: @commit, runner: @runner
-
- stub_gitlab_calls
- visit ci_project_build_path(@project, @build)
- end
-
- it { expect(page).to have_content @commit.sha[0..7] }
- end
- end
-end
diff --git a/spec/features/ci/commits_spec.rb b/spec/features/ci/commits_spec.rb
deleted file mode 100644
index 40a62ca4574..00000000000
--- a/spec/features/ci/commits_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'spec_helper'
-
-describe "Commits" do
- include Ci::CommitsHelper
-
- context "Authenticated user" do
- before do
- @project = FactoryGirl.create :ci_project
- @commit = FactoryGirl.create :ci_commit, project: @project
- @build = FactoryGirl.create :ci_build, commit: @commit
- login_as :user
- @project.gl_project.team << [@user, :master]
- end
-
- describe "GET /:project/commits/:sha" do
- before do
- visit ci_commit_path(@commit)
- end
-
- it { expect(page).to have_content @commit.sha[0..7] }
- it { expect(page).to have_content @commit.git_commit_message }
- it { expect(page).to have_content @commit.git_author_name }
- end
-
- describe "Cancel commit" do
- it "cancels commit" do
- visit ci_commit_path(@commit)
- click_on "Cancel"
-
- expect(page).to have_content "canceled"
- end
- end
-
- describe ".gitlab-ci.yml not found warning" do
- it "does not show warning" do
- visit ci_commit_path(@commit)
-
- expect(page).not_to have_content ".gitlab-ci.yml not found in this commit"
- end
-
- it "shows warning" do
- @commit.push_data[:ci_yaml_file] = nil
- @commit.save
-
- visit ci_commit_path(@commit)
-
- expect(page).to have_content ".gitlab-ci.yml not found in this commit"
- end
- end
- end
-
- context "Public pages" do
- before do
- @project = FactoryGirl.create :ci_public_project
- @commit = FactoryGirl.create :ci_commit, project: @project
- @build = FactoryGirl.create :ci_build, commit: @commit
- end
-
- describe "GET /:project/commits/:sha" do
- before do
- visit ci_commit_path(@commit)
- end
-
- it { expect(page).to have_content @commit.sha[0..7] }
- it { expect(page).to have_content @commit.git_commit_message }
- it { expect(page).to have_content @commit.git_author_name }
- end
- end
-end
diff --git a/spec/features/ci/events_spec.rb b/spec/features/ci/events_spec.rb
deleted file mode 100644
index 5b9fd404159..00000000000
--- a/spec/features/ci/events_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-require 'spec_helper'
-
-describe "Events" do
- let(:user) { create(:user) }
- let(:project) { FactoryGirl.create :ci_project }
- let(:event) { FactoryGirl.create :ci_admin_event, project: project }
-
- before do
- login_as(user)
- project.gl_project.team << [user, :master]
- end
-
- describe "GET /ci/project/:id/events" do
- before do
- event
- visit ci_project_events_path(project)
- end
-
- it { expect(page).to have_content "Events" }
- it { expect(page).to have_content event.description }
- end
-end
diff --git a/spec/features/ci/projects_spec.rb b/spec/features/ci/projects_spec.rb
deleted file mode 100644
index 7c8cd1ce5c7..00000000000
--- a/spec/features/ci/projects_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require 'spec_helper'
-
-describe "Projects" do
- let(:user) { create(:user) }
-
- before do
- login_as(user)
- @project = FactoryGirl.create :ci_project, name: "GitLab / gitlab-shell"
- @project.gl_project.team << [user, :master]
- end
-
- describe "GET /ci/projects/:id" do
- before do
- visit ci_project_path(@project)
- end
-
- it { expect(page).to have_content @project.name }
- it { expect(page).to have_content 'All commits' }
- end
-
- describe "GET /ci/projects/:id/edit" do
- before do
- visit edit_ci_project_path(@project)
- end
-
- it { expect(page).to have_content @project.name }
- it { expect(page).to have_content 'Build Schedule' }
-
- it "updates configuration" do
- fill_in 'Timeout', with: '70'
- click_button 'Save changes'
-
- expect(page).to have_content 'was successfully updated'
-
- expect(find_field('Timeout').value).to eq '70'
- end
- end
-end
diff --git a/spec/features/ci_settings_spec.rb b/spec/features/ci_settings_spec.rb
new file mode 100644
index 00000000000..7e25e883018
--- /dev/null
+++ b/spec/features/ci_settings_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe "CI settings" do
+ let(:user) { create(:user) }
+ before { login_as(user) }
+
+ before do
+ @project = FactoryGirl.create :ci_project
+ @gl_project = @project.gl_project
+ @gl_project.team << [user, :master]
+ visit edit_namespace_project_ci_settings_path(@gl_project.namespace, @gl_project)
+ end
+
+ it { expect(page).to have_content 'Build Schedule' }
+
+ it "updates configuration" do
+ fill_in 'Timeout', with: '70'
+ click_button 'Save changes'
+ expect(page).to have_content 'was successfully updated'
+ expect(find_field('Timeout').value).to eq '70'
+ end
+end
diff --git a/spec/features/ci_web_hooks_spec.rb b/spec/features/ci_web_hooks_spec.rb
new file mode 100644
index 00000000000..efae0a42c1e
--- /dev/null
+++ b/spec/features/ci_web_hooks_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe 'CI web hooks' do
+ let(:user) { create(:user) }
+ before { login_as(user) }
+
+ before do
+ @project = FactoryGirl.create :ci_project
+ @gl_project = @project.gl_project
+ @gl_project.team << [user, :master]
+ visit namespace_project_ci_web_hooks_path(@gl_project.namespace, @gl_project)
+ end
+
+ context 'create a trigger' do
+ before do
+ fill_in 'web_hook_url', with: 'http://example.com'
+ click_on 'Add Web Hook'
+ end
+
+ it { expect(@project.web_hooks.count).to eq(1) }
+
+ it 'revokes the trigger' do
+ click_on 'Remove'
+ expect(@project.web_hooks.count).to eq(0)
+ end
+ end
+end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
new file mode 100644
index 00000000000..90739cd6a28
--- /dev/null
+++ b/spec/features/commits_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe "Commits" do
+ include CiStatusHelper
+
+ let(:project) { create(:project) }
+
+ describe "CI" do
+ before do
+ login_as :user
+ project.team << [@user, :master]
+ @ci_project = project.ensure_gitlab_ci_project
+ @commit = FactoryGirl.create :ci_commit, gl_project: project, sha: project.commit.sha
+ @build = FactoryGirl.create :ci_build, commit: @commit
+ @generic_status = FactoryGirl.create :generic_commit_status, commit: @commit
+ end
+
+ before do
+ stub_ci_commit_to_return_yaml_file
+ end
+
+ describe "GET /:project/commits/:sha/ci" do
+ before do
+ visit ci_status_path(@commit)
+ end
+
+ it { expect(page).to have_content @commit.sha[0..7] }
+ it { expect(page).to have_content @commit.git_commit_message }
+ it { expect(page).to have_content @commit.git_author_name }
+ end
+
+ context "Download artifacts" do
+ let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
+
+ before do
+ @build.update_attributes(artifacts_file: artifacts_file)
+ end
+
+ it do
+ visit ci_status_path(@commit)
+ click_on "Download artifacts"
+ expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type)
+ end
+ end
+
+ describe "Cancel all builds" do
+ it "cancels commit" do
+ visit ci_status_path(@commit)
+ click_on "Cancel running"
+ expect(page).to have_content "canceled"
+ end
+ end
+
+ describe "Cancel build" do
+ it "cancels build" do
+ visit ci_status_path(@commit)
+ click_on "Cancel"
+ expect(page).to have_content "canceled"
+ end
+ end
+
+ describe ".gitlab-ci.yml not found warning" do
+ it "does not show warning" do
+ visit ci_status_path(@commit)
+ expect(page).not_to have_content ".gitlab-ci.yml not found in this commit"
+ end
+
+ it "shows warning" do
+ stub_ci_commit_yaml_file(nil)
+ visit ci_status_path(@commit)
+ expect(page).to have_content ".gitlab-ci.yml not found in this commit"
+ end
+ end
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 32fd4065bb4..0af5e6fc1a6 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -61,7 +61,7 @@ describe 'Issues', feature: true do
it 'allows user to select unasigned', js: true do
visit edit_namespace_project_issue_path(project.namespace, project, issue)
- expect(page).to have_content "Assign to #{@user.name}"
+ expect(page).to have_content "Assignee #{@user.name}"
first('#s2id_issue_assignee_id').click
sleep 2 # wait for ajax stuff to complete
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index cef432e512b..922c76285d1 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -95,7 +95,7 @@ feature 'Login', feature: true do
user = create(:user, password: 'not-the-default')
login_with(user)
- expect(page).to have_content('Invalid email or password.')
+ expect(page).to have_content('Invalid login or password.')
end
end
end
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index c557a1061af..fdd8cf07b12 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -220,7 +220,7 @@ describe 'GitLab Markdown', feature: true do
end
end
- # `markdown` calls these two methods
+ # Fake a `current_user` helper
def current_user
@feat.user
end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index d7cb3b2e86e..f0fc6916c4d 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Comments', feature: true do
include RepoHelpers
+ include WaitForAjax
describe 'On a merge request', js: true, feature: true do
let!(:merge_request) { create(:merge_request) }
@@ -123,8 +124,8 @@ describe 'Comments', feature: true do
it 'removes the attachment div and resets the edit form' do
find('.js-note-attachment-delete').click
is_expected.not_to have_css('.note-attachment')
- expect(find('.current-note-edit-form', visible: false)).
- not_to be_visible
+ is_expected.not_to have_css('.current-note-edit-form')
+ wait_for_ajax
end
end
end
diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb
index 2b6311e4fd7..85e70b4d47f 100644
--- a/spec/features/password_reset_spec.rb
+++ b/spec/features/password_reset_spec.rb
@@ -1,53 +1,43 @@
require 'spec_helper'
feature 'Password reset', feature: true do
- def forgot_password
- click_on 'Forgot your password?'
- fill_in 'Email', with: user.email
- click_button 'Reset password'
- user.reload
- end
-
- def get_reset_token
- mail = ActionMailer::Base.deliveries.last
- body = mail.body.encoded
- body.scan(/reset_password_token=(.+)\"/).flatten.first
- end
-
- def reset_password(password = 'password')
- visit edit_user_password_path(reset_password_token: get_reset_token)
+ describe 'throttling' do
+ it 'sends reset instructions when not previously sent' do
+ visit root_path
+ forgot_password(create(:user))
- fill_in 'New password', with: password
- fill_in 'Confirm new password', with: password
- click_button 'Change your password'
- end
+ expect(page).to have_content(I18n.t('devise.passwords.send_instructions'))
+ expect(current_path).to eq new_user_session_path
+ end
- describe 'with two-factor authentication' do
- let(:user) { create(:user, :two_factor) }
+ it 'sends reset instructions when previously sent more than a minute ago' do
+ user = create(:user)
+ user.send_reset_password_instructions
+ user.update_attribute(:reset_password_sent_at, 5.minutes.ago)
- it 'requires login after password reset' do
visit root_path
+ forgot_password(user)
- forgot_password
- reset_password
-
- expect(page).to have_content("Your password was changed successfully.")
- expect(page).not_to have_content("You are now signed in.")
+ expect(page).to have_content(I18n.t('devise.passwords.send_instructions'))
expect(current_path).to eq new_user_session_path
end
- end
- describe 'without two-factor authentication' do
- let(:user) { create(:user) }
+ it "throttles multiple resets in a short timespan" do
+ user = create(:user)
+ user.send_reset_password_instructions
- it 'automatically logs in after password reset' do
visit root_path
+ forgot_password(user)
- forgot_password
- reset_password
-
- expect(current_path).to eq root_path
- expect(page).to have_content("Your password was changed successfully. You are now signed in.")
+ expect(page).to have_content(I18n.t('devise.passwords.recently_reset'))
+ expect(current_path).to eq new_user_password_path
end
end
+
+ def forgot_password(user)
+ click_on 'Forgot your password?'
+ fill_in 'Email', with: user.email
+ click_button 'Reset password'
+ user.reload
+ end
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index a362c54b9ad..09fcff2444a 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -34,6 +34,27 @@ feature 'Project', feature: true do
end
end
+ describe 'remove forked relationship', js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ before do
+ login_with user
+ create(:forked_project_link, forked_to_project: project)
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ it 'should remove fork' do
+ expect(page).to have_content 'Remove fork relationship'
+
+ remove_with_confirm('Remove fork relationship', project.path)
+
+ expect(page).to have_content 'The fork relationship has been removed.'
+ expect(project.forked?).to be_falsey
+ expect(page).not_to have_content 'Remove fork relationship'
+ end
+ end
+
describe 'removal', js: true do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
@@ -45,13 +66,13 @@ feature 'Project', feature: true do
end
it 'should remove project' do
- expect { remove_project }.to change {Project.count}.by(-1)
+ expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1)
end
end
- def remove_project
- click_link "Remove project"
- fill_in 'confirm_name_input', with: project.path
+ def remove_with_confirm(button_text, confirm_with)
+ click_button button_text
+ fill_in 'confirm_name_input', with: confirm_with
click_button 'Confirm'
end
end
diff --git a/spec/features/ci/runners_spec.rb b/spec/features/runners_spec.rb
index 15147f15eb3..b0259026630 100644
--- a/spec/features/ci/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -1,11 +1,10 @@
require 'spec_helper'
describe "Runners" do
- let(:user) { create(:user) }
+ include GitlabRoutingHelper
- before do
- login_as(user)
- end
+ let(:user) { create(:user) }
+ before { login_as(user) }
describe "specific runners" do
before do
@@ -15,23 +14,32 @@ describe "Runners" do
@project2 = FactoryGirl.create :ci_project
@project2.gl_project.team << [user, :master]
+ @project3 = FactoryGirl.create :ci_project
+ @project3.gl_project.team << [user, :developer]
+
@shared_runner = FactoryGirl.create :ci_shared_runner
@specific_runner = FactoryGirl.create :ci_specific_runner
@specific_runner2 = FactoryGirl.create :ci_specific_runner
+ @specific_runner3 = FactoryGirl.create :ci_specific_runner
@project.runners << @specific_runner
@project2.runners << @specific_runner2
+ @project3.runners << @specific_runner3
+
+ visit runners_path(@project.gl_project)
+ end
+
+ before do
+ expect(page).to_not have_content(@specific_runner3.display_name)
+ expect(page).to_not have_content(@specific_runner3.display_name)
end
it "places runners in right places" do
- visit ci_project_runners_path(@project)
expect(page.find(".available-specific-runners")).to have_content(@specific_runner2.display_name)
expect(page.find(".activated-specific-runners")).to have_content(@specific_runner.display_name)
expect(page.find(".available-shared-runners")).to have_content(@shared_runner.display_name)
end
it "enables specific runner for project" do
- visit ci_project_runners_path(@project)
-
within ".available-specific-runners" do
click_on "Enable for this project"
end
@@ -41,8 +49,7 @@ describe "Runners" do
it "disables specific runner for project" do
@project2.runners << @specific_runner
-
- visit ci_project_runners_path(@project)
+ visit runners_path(@project.gl_project)
within ".activated-specific-runners" do
click_on "Disable for this project"
@@ -52,8 +59,6 @@ describe "Runners" do
end
it "removes specific runner for project if this is last project for that runners" do
- visit ci_project_runners_path(@project)
-
within ".activated-specific-runners" do
click_on "Remove runner"
end
@@ -66,13 +71,11 @@ describe "Runners" do
before do
@project = FactoryGirl.create :ci_project
@project.gl_project.team << [user, :master]
+ visit runners_path(@project.gl_project)
end
it "enables shared runners" do
- visit ci_project_runners_path(@project)
-
click_on "Enable shared runners"
-
expect(@project.reload.shared_runners_enabled).to be_truthy
end
end
@@ -86,10 +89,8 @@ describe "Runners" do
end
it "shows runner information" do
- visit ci_project_runners_path(@project)
-
+ visit runners_path(@project.gl_project)
click_on @specific_runner.short_sha
-
expect(page).to have_content(@specific_runner.platform)
end
end
diff --git a/spec/features/ci/triggers_spec.rb b/spec/features/triggers_spec.rb
index c6afeb74628..69492d58878 100644
--- a/spec/features/ci/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -1,13 +1,14 @@
require 'spec_helper'
describe 'Triggers' do
- let(:user) { create(:user) }
+ let(:user) { create(:user) }
+ before { login_as(user) }
before do
- login_as(user)
@project = FactoryGirl.create :ci_project
- @project.gl_project.team << [user, :master]
- visit ci_project_triggers_path(@project)
+ @gl_project = @project.gl_project
+ @gl_project.team << [user, :master]
+ visit namespace_project_triggers_path(@gl_project.namespace, @gl_project)
end
context 'create a trigger' do
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index efcb8a31abe..c1248162031 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Users', feature: true do
+ let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') }
+
scenario 'GET /users/sign_in creates a new user account' do
visit new_user_session_path
fill_in 'user_name', with: 'Name Surname'
@@ -11,7 +13,6 @@ feature 'Users', feature: true do
end
scenario 'Successful user signin invalidates password reset token' do
- user = create(:user)
expect(user.reset_password_token).to be_nil
visit new_user_password_path
@@ -28,7 +29,6 @@ feature 'Users', feature: true do
expect(user.reset_password_token).to be_nil
end
- let!(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') }
scenario 'Should show one error if email is already taken' do
visit new_user_session_path
fill_in 'user_name', with: 'Another user name'
diff --git a/spec/features/ci/variables_spec.rb b/spec/features/variables_spec.rb
index e387b3be555..adb602f3edd 100644
--- a/spec/features/ci/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -1,28 +1,25 @@
require 'spec_helper'
describe "Variables" do
- let(:user) { create(:user) }
-
- before do
- login_as(user)
- end
+ let(:user) { create(:user) }
+ before { login_as(user) }
describe "specific runners" do
before do
@project = FactoryGirl.create :ci_project
- @project.gl_project.team << [user, :master]
+ @gl_project = @project.gl_project
+ @gl_project.team << [user, :master]
end
it "creates variable", js: true do
- visit ci_project_variables_path(@project)
+ visit namespace_project_variables_path(@gl_project.namespace, @gl_project)
click_on "Add a variable"
fill_in "Key", with: "SECRET_KEY"
fill_in "Value", with: "SECRET_VALUE"
click_on "Save changes"
-
+
expect(page).to have_content("Variables were successfully updated.")
expect(@project.variables.count).to eq(1)
end
-
end
end
diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb
new file mode 100644
index 00000000000..65d7f14c721
--- /dev/null
+++ b/spec/finders/contributed_projects_finder_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe ContributedProjectsFinder do
+ let(:source_user) { create(:user) }
+ let(:current_user) { create(:user) }
+
+ let(:finder) { described_class.new(source_user) }
+
+ let!(:public_project) { create(:project, :public) }
+ let!(:private_project) { create(:project, :private) }
+
+ before do
+ private_project.team << [source_user, Gitlab::Access::MASTER]
+ private_project.team << [current_user, Gitlab::Access::DEVELOPER]
+ public_project.team << [source_user, Gitlab::Access::MASTER]
+
+ create(:event, action: Event::PUSHED, project: public_project,
+ target: public_project, author: source_user)
+
+ create(:event, action: Event::PUSHED, project: private_project,
+ target: private_project, author: source_user)
+ end
+
+ describe 'without a current user' do
+ subject { finder.execute }
+
+ it { is_expected.to eq([public_project]) }
+ end
+
+ describe 'with a current user' do
+ subject { finder.execute(current_user) }
+
+ it { is_expected.to eq([private_project, public_project]) }
+ end
+end
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
new file mode 100644
index 00000000000..4f6a000822e
--- /dev/null
+++ b/spec/finders/groups_finder_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe GroupsFinder do
+ describe '#execute' do
+ let(:user) { create(:user) }
+
+ let(:group1) { create(:group) }
+ let(:group2) { create(:group) }
+ let(:group3) { create(:group) }
+ let(:group4) { create(:group, public: true) }
+
+ let!(:public_project) { create(:project, :public, group: group1) }
+ let!(:internal_project) { create(:project, :internal, group: group2) }
+ let!(:private_project) { create(:project, :private, group: group3) }
+
+ let(:finder) { described_class.new }
+
+ describe 'with a user' do
+ subject { finder.execute(user) }
+
+ describe 'when the user is not a member of any groups' do
+ it { is_expected.to eq([group4, group2, group1]) }
+ end
+
+ describe 'when the user is a member of a group' do
+ before do
+ group3.add_user(user, Gitlab::Access::DEVELOPER)
+ end
+
+ it { is_expected.to eq([group4, group3, group2, group1]) }
+ end
+
+ describe 'when the user is a member of a private project' do
+ before do
+ private_project.team.add_user(user, Gitlab::Access::DEVELOPER)
+ end
+
+ it { is_expected.to eq([group4, group3, group2, group1]) }
+ end
+ end
+
+ describe 'without a user' do
+ subject { finder.execute }
+
+ it { is_expected.to eq([group4, group1]) }
+ end
+ end
+end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index db20b23f87d..b1648055462 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -6,9 +6,11 @@ describe IssuesFinder do
let(:project1) { create(:project) }
let(:project2) { create(:project) }
let(:milestone) { create(:milestone, project: project1) }
+ let(:label) { create(:label, project: project2) }
let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) }
let(:issue2) { create(:issue, author: user, assignee: user, project: project2) }
let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) }
+ let!(:label_link) { create(:label_link, label: label, target: issue2) }
before do
project1.team << [user, :master]
@@ -48,6 +50,24 @@ describe IssuesFinder do
expect(issues).to eq([issue1])
end
+ it 'should filter by no milestone id' do
+ params = { scope: "all", milestone_title: Milestone::None.title, state: 'opened' }
+ issues = IssuesFinder.new(user, params).execute
+ expect(issues).to match_array([issue2, issue3])
+ end
+
+ it 'should filter by label name' do
+ params = { scope: "all", label_name: label.title, state: 'opened' }
+ issues = IssuesFinder.new(user, params).execute
+ expect(issues).to eq([issue2])
+ end
+
+ it 'should filter by no label name' do
+ params = { scope: "all", label_name: Label::None.title, state: 'opened' }
+ issues = IssuesFinder.new(user, params).execute
+ expect(issues).to match_array([issue1, issue3])
+ end
+
it 'should be empty for unauthorized user' do
params = { scope: "all", state: 'opened' }
issues = IssuesFinder.new(nil, params).execute
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
new file mode 100644
index 00000000000..2d9068cc720
--- /dev/null
+++ b/spec/finders/joined_groups_finder_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe JoinedGroupsFinder do
+ describe '#execute' do
+ let(:source_user) { create(:user) }
+ let(:current_user) { create(:user) }
+
+ let(:group1) { create(:group) }
+ let(:group2) { create(:group) }
+ let(:group3) { create(:group) }
+ let(:group4) { create(:group, public: true) }
+
+ let!(:public_project) { create(:project, :public, group: group1) }
+ let!(:internal_project) { create(:project, :internal, group: group2) }
+ let!(:private_project) { create(:project, :private, group: group3) }
+
+ let(:finder) { described_class.new(source_user) }
+
+ before do
+ [group1, group2, group3, group4].each do |group|
+ group.add_user(source_user, Gitlab::Access::MASTER)
+ end
+ end
+
+ describe 'with a current user' do
+ describe 'when the current user has access to the projects of the source user' do
+ before do
+ private_project.team.add_user(current_user, Gitlab::Access::DEVELOPER)
+ end
+
+ subject { finder.execute(current_user) }
+
+ it { is_expected.to eq([group4, group3, group2, group1]) }
+ end
+
+ describe 'when the current user does not have access to the projects of the source user' do
+ subject { finder.execute(current_user) }
+
+ it { is_expected.to eq([group4, group2, group1]) }
+ end
+ end
+
+ describe 'without a current user' do
+ subject { finder.execute }
+
+ it { is_expected.to eq([group4, group1]) }
+ end
+ end
+end
diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb
new file mode 100644
index 00000000000..38817add456
--- /dev/null
+++ b/spec/finders/personal_projects_finder_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe PersonalProjectsFinder do
+ let(:source_user) { create(:user) }
+ let(:current_user) { create(:user) }
+
+ let(:finder) { described_class.new(source_user) }
+
+ let!(:public_project) do
+ create(:project, :public, namespace: source_user.namespace, name: 'A',
+ path: 'A')
+ end
+
+ let!(:private_project) do
+ create(:project, :private, namespace: source_user.namespace, name: 'B',
+ path: 'B')
+ end
+
+ before do
+ private_project.team << [current_user, Gitlab::Access::DEVELOPER]
+ end
+
+ describe 'without a current user' do
+ subject { finder.execute }
+
+ it { is_expected.to eq([public_project]) }
+ end
+
+ describe 'with a current user' do
+ subject { finder.execute(current_user) }
+
+ it { is_expected.to eq([private_project, public_project]) }
+ end
+end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index de9d4cd6128..f32641ef0f6 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -1,51 +1,63 @@
require 'spec_helper'
describe ProjectsFinder do
- let(:user) { create :user }
- let(:group) { create :group }
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
- let(:project1) { create(:empty_project, :public, group: group) }
- let(:project2) { create(:empty_project, :internal, group: group) }
- let(:project3) { create(:empty_project, :private, group: group) }
- let(:project4) { create(:empty_project, :private, group: group) }
+ let!(:private_project) do
+ create(:project, :private, name: 'A', path: 'A')
+ end
- context 'non authenticated' do
- subject { ProjectsFinder.new.execute(nil, group: group) }
+ let!(:internal_project) do
+ create(:project, :internal, group: group, name: 'B', path: 'B')
+ end
- it { is_expected.to include(project1) }
- it { is_expected.not_to include(project2) }
- it { is_expected.not_to include(project3) }
- it { is_expected.not_to include(project4) }
- end
+ let!(:public_project) do
+ create(:project, :public, group: group, name: 'C', path: 'C')
+ end
- context 'authenticated' do
- subject { ProjectsFinder.new.execute(user, group: group) }
+ let(:finder) { described_class.new }
- it { is_expected.to include(project1) }
- it { is_expected.to include(project2) }
- it { is_expected.not_to include(project3) }
- it { is_expected.not_to include(project4) }
- end
+ describe 'without a group' do
+ describe 'without a user' do
+ subject { finder.execute }
- context 'authenticated, project member' do
- before { project3.team << [user, :developer] }
+ it { is_expected.to eq([public_project]) }
+ end
- subject { ProjectsFinder.new.execute(user, group: group) }
+ describe 'with a user' do
+ subject { finder.execute(user) }
- it { is_expected.to include(project1) }
- it { is_expected.to include(project2) }
- it { is_expected.to include(project3) }
- it { is_expected.not_to include(project4) }
- end
+ describe 'without private projects' do
+ it { is_expected.to eq([public_project, internal_project]) }
+ end
+
+ describe 'with private projects' do
+ before do
+ private_project.team.add_user(user, Gitlab::Access::MASTER)
+ end
+
+ it do
+ is_expected.to eq([public_project, internal_project,
+ private_project])
+ end
+ end
+ end
+ end
+
+ describe 'with a group' do
+ describe 'without a user' do
+ subject { finder.execute(nil, group: group) }
- context 'authenticated, group member' do
- before { group.add_developer(user) }
+ it { is_expected.to eq([public_project]) }
+ end
- subject { ProjectsFinder.new.execute(user, group: group) }
+ describe 'with a user' do
+ subject { finder.execute(user, group: group) }
- it { is_expected.to include(project1) }
- it { is_expected.to include(project2) }
- it { is_expected.to include(project3) }
- it { is_expected.to include(project4) }
+ it { is_expected.to eq([public_project, internal_project]) }
+ end
+ end
end
end
diff --git a/spec/finders/trending_projects_finder_spec.rb b/spec/finders/trending_projects_finder_spec.rb
new file mode 100644
index 00000000000..a49cbfd5160
--- /dev/null
+++ b/spec/finders/trending_projects_finder_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe TrendingProjectsFinder do
+ let(:user) { build(:user) }
+
+ describe '#execute' do
+ describe 'without an explicit start date' do
+ subject { described_class.new }
+
+ it 'returns the trending projects' do
+ relation = double(:ar_relation)
+
+ allow(subject).to receive(:projects_for)
+ .with(user)
+ .and_return(relation)
+
+ allow(relation).to receive(:trending)
+ .with(an_instance_of(ActiveSupport::TimeWithZone))
+ end
+ end
+
+ describe 'with an explicit start date' do
+ let(:date) { 2.months.ago }
+
+ subject { described_class.new }
+
+ it 'returns the trending projects' do
+ relation = double(:ar_relation)
+
+ allow(subject).to receive(:projects_for)
+ .with(user)
+ .and_return(relation)
+
+ allow(relation).to receive(:trending)
+ .with(date)
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 41d12afa9ce..e8dfc5c0eb1 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -153,6 +153,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Ignores invalid: <%= User.reference_prefix %>fake_user
- Ignored in code: `<%= user.to_reference %>`
- Ignored in links: [Link to <%= user.to_reference %>](#user-link)
+- Link to user by reference: [User](<%= user.to_reference %>)
#### IssueReferenceFilter
@@ -160,6 +161,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Issue in another project: <%= xissue.to_reference(project) %>
- Ignored in code: `<%= issue.to_reference %>`
- Ignored in links: [Link to <%= issue.to_reference %>](#issue-link)
+- Issue by URL: <%= urls.namespace_project_issue_url(issue.project.namespace, issue.project, issue) %>
+- Link to issue by reference: [Issue](<%= issue.to_reference %>)
+- Link to issue by URL: [Issue](<%= urls.namespace_project_issue_url(issue.project.namespace, issue.project, issue) %>)
#### MergeRequestReferenceFilter
@@ -167,6 +171,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Merge request in another project: <%= xmerge_request.to_reference(project) %>
- Ignored in code: `<%= merge_request.to_reference %>`
- Ignored in links: [Link to <%= merge_request.to_reference %>](#merge-request-link)
+- Merge request by URL: <%= urls.namespace_project_merge_request_url(merge_request.project.namespace, merge_request.project, merge_request) %>
+- Link to merge request by reference: [Merge request](<%= merge_request.to_reference %>)
+- Link to merge request by URL: [Merge request](<%= urls.namespace_project_merge_request_url(merge_request.project.namespace, merge_request.project, merge_request) %>)
#### SnippetReferenceFilter
@@ -174,6 +181,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Snippet in another project: <%= xsnippet.to_reference(project) %>
- Ignored in code: `<%= snippet.to_reference %>`
- Ignored in links: [Link to <%= snippet.to_reference %>](#snippet-link)
+- Snippet by URL: <%= urls.namespace_project_snippet_url(snippet.project.namespace, snippet.project, snippet) %>
+- Link to snippet by reference: [Snippet](<%= snippet.to_reference %>)
+- Link to snippet by URL: [Snippet](<%= urls.namespace_project_snippet_url(snippet.project.namespace, snippet.project, snippet) %>)
#### CommitRangeReferenceFilter
@@ -181,6 +191,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Range in another project: <%= xcommit_range.to_reference(project) %>
- Ignored in code: `<%= commit_range.to_reference %>`
- Ignored in links: [Link to <%= commit_range.to_reference %>](#commit-range-link)
+- Range by URL: <%= urls.namespace_project_compare_url(commit_range.project.namespace, commit_range.project, commit_range.to_param) %>
+- Link to range by reference: [Range](<%= commit_range.to_reference %>)
+- Link to range by URL: [Range](<%= urls.namespace_project_compare_url(commit_range.project.namespace, commit_range.project, commit_range.to_param) %>)
#### CommitReferenceFilter
@@ -188,6 +201,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Commit in another project: <%= xcommit.to_reference(project) %>
- Ignored in code: `<%= commit.to_reference %>`
- Ignored in links: [Link to <%= commit.to_reference %>](#commit-link)
+- Commit by URL: <%= urls.namespace_project_commit_url(commit.project.namespace, commit.project, commit) %>
+- Link to commit by reference: [Commit](<%= commit.to_reference %>)
+- Link to commit by URL: [Commit](<%= urls.namespace_project_commit_url(commit.project.namespace, commit.project, commit) %>)
#### LabelReferenceFilter
@@ -196,6 +212,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Label by name in quotes: <%= label.to_reference(:name) %>
- Ignored in code: `<%= simple_label.to_reference %>`
- Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link)
+- Link to label by reference: [Label](<%= label.to_reference %>)
### Task Lists
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 742420f550e..5568f06639c 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -59,7 +59,7 @@ describe ApplicationHelper do
avatar_url = "http://localhost/uploads/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).
- to eq "<img alt=\"Banana sample\" src=\"#{avatar_url}\" />"
+ to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
end
it 'should give uploaded icon when present' do
@@ -95,9 +95,18 @@ describe ApplicationHelper do
end
it 'should call gravatar_icon when no User exists with the given email' do
- expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20)
+ expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
- helper.avatar_icon('foo@example.com', 20)
+ helper.avatar_icon('foo@example.com', 20, 2)
+ end
+
+ describe 'using a User' do
+ it 'should return an URL for the avatar' do
+ user = create(:user, avatar: File.open(avatar_file_path))
+
+ expect(helper.avatar_icon(user).to_s).
+ to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
+ end
end
end
@@ -141,15 +150,19 @@ describe ApplicationHelper do
stub_gravatar_setting(plain_url: 'http://example.local/?s=%{size}&hash=%{hash}')
expect(gravatar_icon(user_email, 20)).
- to eq('http://example.local/?s=20&hash=b58c6f14d292556214bd64909bcdb118')
+ to eq('http://example.local/?s=40&hash=b58c6f14d292556214bd64909bcdb118')
end
it 'accepts a custom size argument' do
- expect(helper.gravatar_icon(user_email, 64)).to include '?s=64'
+ expect(helper.gravatar_icon(user_email, 64)).to include '?s=128'
+ end
+
+ it 'defaults size to 40@2x when given an invalid size' do
+ expect(helper.gravatar_icon(user_email, nil)).to include '?s=80'
end
- it 'defaults size to 40 when given an invalid size' do
- expect(helper.gravatar_icon(user_email, nil)).to include '?s=40'
+ it 'accepts a scaling factor' do
+ expect(helper.gravatar_icon(user_email, 40, 3)).to include '?s=120'
end
it 'ignores case and surrounding whitespace' do
@@ -265,7 +278,7 @@ describe ApplicationHelper do
el = element.next_element
expect(el.name).to eq 'script'
- expect(el.text).to include "$('.js-timeago').timeago()"
+ expect(el.text).to include "$('.js-timeago').last().timeago()"
end
it 'allows the script tag to be excluded' do
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index be0e0c747b7..762ec25c4f5 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -11,12 +11,15 @@ describe GitlabMarkdownHelper do
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:snippet) { create(:project_snippet, project: project) }
- # Helper expects a current_user method.
- let(:current_user) { user }
-
before do
+ # Ensure the generated reference links aren't redacted
+ project.team << [user, :master]
+
# Helper expects a @project instance variable
- @project = project
+ helper.instance_variable_set(:@project, project)
+
+ # Stub the `current_user` helper
+ allow(helper).to receive(:current_user).and_return(user)
end
describe "#markdown" do
@@ -25,23 +28,23 @@ describe GitlabMarkdownHelper do
it "should link to the merge request" do
expected = namespace_project_merge_request_path(project.namespace, project, merge_request)
- expect(markdown(actual)).to match(expected)
+ expect(helper.markdown(actual)).to match(expected)
end
it "should link to the commit" do
expected = namespace_project_commit_path(project.namespace, project, commit)
- expect(markdown(actual)).to match(expected)
+ expect(helper.markdown(actual)).to match(expected)
end
it "should link to the issue" do
expected = namespace_project_issue_path(project.namespace, project, issue)
- expect(markdown(actual)).to match(expected)
+ expect(helper.markdown(actual)).to match(expected)
end
end
describe "override default project" do
let(:actual) { issue.to_reference }
- let(:second_project) { create(:project) }
+ let(:second_project) { create(:project, :public) }
let(:second_issue) { create(:issue, project: second_project) }
it 'should link to the issue' do
@@ -56,7 +59,7 @@ describe GitlabMarkdownHelper do
let(:issues) { create_list(:issue, 2, project: project) }
it 'should handle references nested in links with all the text' do
- actual = link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path)
+ actual = helper.link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path)
doc = Nokogiri::HTML.parse(actual)
# Make sure we didn't create invalid markup
@@ -86,7 +89,7 @@ describe GitlabMarkdownHelper do
end
it 'should forward HTML options' do
- actual = link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
+ actual = helper.link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
doc = Nokogiri::HTML.parse(actual)
expect(doc.css('a')).to satisfy do |v|
@@ -97,15 +100,21 @@ describe GitlabMarkdownHelper do
it "escapes HTML passed in as the body" do
actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}"
- expect(link_to_gfm(actual, commit_path)).
+ expect(helper.link_to_gfm(actual, commit_path)).
to match('&lt;h1&gt;test&lt;/h1&gt;')
end
it 'ignores reference links when they are the entire body' do
text = issues[0].to_reference
- act = link_to_gfm(text, '/foo')
+ act = helper.link_to_gfm(text, '/foo')
expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>)
end
+
+ it 'should replace commit message with emoji to link' do
+ actual = link_to_gfm(':book:Book', '/foo')
+ expect(actual).
+ to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://localhost/assets/emoji/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
+ end
end
describe '#render_wiki_content' do
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index c08ddb4cae1..1f2c4ee77b5 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -117,4 +117,40 @@ describe IssuesHelper do
end
end
+ describe "#merge_requests_sentence" do
+ subject { merge_requests_sentence(merge_requests)}
+ let(:merge_requests) do
+ [ build(:merge_request, iid: 1), build(:merge_request, iid: 2),
+ build(:merge_request, iid: 3)]
+ end
+
+ it { is_expected.to eq("!1, !2, or !3") }
+ end
+
+ describe "#url_to_emoji" do
+ it "returns url" do
+ expect(url_to_emoji("smile")).to include("emoji/1F604.png")
+ end
+ end
+
+ describe "#emoji_list" do
+ it "returns url" do
+ expect(emoji_list).to be_kind_of(Array)
+ end
+ end
+
+ describe "#note_active_class" do
+ before do
+ @note = create :note
+ @note1 = create :note
+ end
+
+ it "returns empty string for unauthenticated user" do
+ expect(note_active_class(Note.all, nil)).to eq("")
+ end
+
+ it "returns active string for author" do
+ expect(note_active_class(Note.all, @note.author)).to eq("active")
+ end
+ end
end
diff --git a/spec/helpers/merge_requests_helper.rb b/spec/helpers/merge_requests_helper.rb
deleted file mode 100644
index 5262d644048..00000000000
--- a/spec/helpers/merge_requests_helper.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'spec_helper'
-
-describe MergeRequestsHelper do
- describe :issues_sentence do
- subject { issues_sentence(issues) }
- let(:issues) do
- [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)]
- end
-
- it { is_expected.to eq('#1, #2, and #3') }
- end
-end
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
new file mode 100644
index 00000000000..0ef1efb8bce
--- /dev/null
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe MergeRequestsHelper do
+ describe "#issues_sentence" do
+ subject { issues_sentence(issues) }
+ let(:issues) do
+ [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)]
+ end
+
+ it { is_expected.to eq('#1, #2, and #3') }
+ end
+
+ describe "#format_mr_branch_names" do
+ describe "within the same project" do
+ let(:merge_request) { create(:merge_request) }
+ subject { format_mr_branch_names(merge_request) }
+
+ it { is_expected.to eq([merge_request.source_branch, merge_request.target_branch]) }
+ end
+
+ describe "within different projects" do
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) }
+ subject { format_mr_branch_names(merge_request) }
+ let(:source_title) { "#{fork_project.path_with_namespace}:#{merge_request.source_branch}" }
+ let(:target_title) { "#{project.path_with_namespace}:#{merge_request.target_branch}" }
+
+ it { is_expected.to eq([source_title, target_title]) }
+ end
+ end
+end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 06f69262b71..e5df59c4fba 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -8,14 +8,18 @@ describe PreferencesHelper do
end
it 'raises an exception when defined choices may be using the wrong key' do
- expect(User).to receive(:dashboards).and_return(foo: 'foo', bar: 'bar')
+ dashboards = User.dashboards.dup
+ dashboards[:projects_changed] = dashboards.delete :projects
+ expect(User).to receive(:dashboards).and_return(dashboards)
expect { helper.dashboard_choices }.to raise_error(KeyError)
end
it 'provides better option descriptions' do
expect(helper.dashboard_choices).to match_array [
['Your Projects (default)', 'projects'],
- ['Starred Projects', 'stars']
+ ['Starred Projects', 'stars'],
+ ["Your Projects' Activity", 'project_activity'],
+ ["Starred Projects' Activity", 'starred_project_activity']
]
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 99abb95d906..f2efb528aeb 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -61,13 +61,27 @@ describe ProjectsHelper do
end
it "returns a valid cach key" do
- expect(helper.send(:readme_cache_key)).to eq("#{project.id}-#{project.commit.id}-readme")
+ expect(helper.send(:readme_cache_key)).to eq("#{project.path_with_namespace}-#{project.commit.id}-readme")
end
it "returns a valid cache key if HEAD does not exist" do
allow(project).to receive(:commit) { nil }
- expect(helper.send(:readme_cache_key)).to eq("#{project.id}-nil-readme")
+ expect(helper.send(:readme_cache_key)).to eq("#{project.path_with_namespace}-nil-readme")
+ end
+ end
+
+ describe 'link_to_member' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, group: group) }
+ let(:user) { create(:user) }
+
+ describe 'using the default options' do
+ it 'returns an HTML link to the user' do
+ link = helper.link_to_member(project, user)
+
+ expect(link).to match(%r{/u/#{user.username}})
+ end
end
end
end
diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/runners_helper_spec.rb
index 6d0e2d3d1e1..35f91b7decf 100644
--- a/spec/helpers/ci/runners_helper_spec.rb
+++ b/spec/helpers/runners_helper_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::RunnersHelper do
+describe RunnersHelper do
it "returns - not contacted yet" do
runner = FactoryGirl.build :ci_runner
expect(runner_status_icon(runner)).to include("not connected yet")
@@ -12,7 +12,7 @@ describe Ci::RunnersHelper do
end
it "returns online text" do
- runner = FactoryGirl.build(:ci_runner, contacted_at: 1.hour.ago, active: true)
+ runner = FactoryGirl.build(:ci_runner, contacted_at: 1.second.ago, active: true)
expect(runner_status_icon(runner)).to include("Runner is online")
end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index b327f4f911a..ebe9c29d91c 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -42,6 +42,11 @@ describe SearchHelper do
expect(search_autocomplete_opts(project.name).size).to eq(1)
end
+ it "includes the public group" do
+ group = create(:group, public: true)
+ expect(search_autocomplete_opts(group.name).size).to eq(1)
+ end
+
context "with a current project" do
before { @project = create(:project) }
diff --git a/spec/helpers/ci/application_helper_spec.rb b/spec/helpers/time_helper_spec.rb
index 6a216715b7f..3f62527c5bb 100644
--- a/spec/helpers/ci/application_helper_spec.rb
+++ b/spec/helpers/time_helper_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::ApplicationHelper do
+describe TimeHelper do
describe "#duration_in_words" do
it "returns minutes and seconds" do
intervals_in_words = {
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee
new file mode 100644
index 00000000000..09708c12ed4
--- /dev/null
+++ b/spec/javascripts/behaviors/quick_submit_spec.js.coffee
@@ -0,0 +1,70 @@
+#= require behaviors/quick_submit
+
+describe 'Quick Submit behavior', ->
+ fixture.preload('behaviors/quick_submit.html')
+
+ beforeEach ->
+ fixture.load('behaviors/quick_submit.html')
+
+ # Prevent a form submit from moving us off the testing page
+ $('form').submit (e) -> e.preventDefault()
+
+ @spies = {
+ submit: spyOnEvent('form', 'submit')
+ }
+
+ it 'does not respond to other keyCodes', ->
+ $('input').trigger(keydownEvent(keyCode: 32))
+
+ expect(@spies.submit).not.toHaveBeenTriggered()
+
+ it 'does not respond to Enter alone', ->
+ $('input').trigger(keydownEvent(ctrlKey: false, metaKey: false))
+
+ expect(@spies.submit).not.toHaveBeenTriggered()
+
+ it 'does not respond to repeated events', ->
+ $('input').trigger(keydownEvent(repeat: true))
+
+ expect(@spies.submit).not.toHaveBeenTriggered()
+
+ it 'disables submit buttons', ->
+ $('textarea').trigger(keydownEvent())
+
+ expect($('input[type=submit]')).toBeDisabled()
+ expect($('button[type=submit]')).toBeDisabled()
+
+ # We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll
+ # only run the tests that apply to the current platform
+ if navigator.userAgent.match(/Macintosh/)
+ it 'responds to Meta+Enter', ->
+ $('input').trigger(keydownEvent())
+
+ expect(@spies.submit).toHaveBeenTriggered()
+
+ it 'excludes other modifier keys', ->
+ $('input').trigger(keydownEvent(altKey: true))
+ $('input').trigger(keydownEvent(ctrlKey: true))
+ $('input').trigger(keydownEvent(shiftKey: true))
+
+ expect(@spies.submit).not.toHaveBeenTriggered()
+ else
+ it 'responds to Ctrl+Enter', ->
+ $('input').trigger(keydownEvent())
+
+ expect(@spies.submit).toHaveBeenTriggered()
+
+ it 'excludes other modifier keys', ->
+ $('input').trigger(keydownEvent(altKey: true))
+ $('input').trigger(keydownEvent(metaKey: true))
+ $('input').trigger(keydownEvent(shiftKey: true))
+
+ expect(@spies.submit).not.toHaveBeenTriggered()
+
+ keydownEvent = (options) ->
+ if navigator.userAgent.match(/Macintosh/)
+ defaults = { keyCode: 13, metaKey: true }
+ else
+ defaults = { keyCode: 13, ctrlKey: true }
+
+ $.Event('keydown', $.extend({}, defaults, options))
diff --git a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
new file mode 100644
index 00000000000..b80a28a33ea
--- /dev/null
+++ b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
@@ -0,0 +1,6 @@
+%form{ action: '/foo' }
+ %input.js-quick-submit{ type: 'text' }
+ %textarea.js-quick-submit
+
+ %input{ type: 'submit'} Submit
+ %button.btn{ type: 'submit' } Submit
diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml
index da1ebcdb23c..514877340e4 100644
--- a/spec/javascripts/fixtures/line_highlighter.html.haml
+++ b/spec/javascripts/fixtures/line_highlighter.html.haml
@@ -1,4 +1,4 @@
-#tree-content-holder
+#blob-content-holder
.file-content
.line-numbers
- 1.upto(25) do |i|
diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee
index 57453c716a5..a073f21e7bc 100644
--- a/spec/javascripts/line_highlighter_spec.js.coffee
+++ b/spec/javascripts/line_highlighter_spec.js.coffee
@@ -39,7 +39,7 @@ describe 'LineHighlighter', ->
expect(spy).toHaveBeenPrevented()
it 'handles garbage input from the hash', ->
- func = -> new LineHighlighter('#tree-content-holder')
+ func = -> new LineHighlighter('#blob-content-holder')
expect(func).not.toThrow()
describe '#clickHandler', ->
diff --git a/spec/javascripts/spec_helper.coffee b/spec/javascripts/spec_helper.coffee
index 47b41dd2c81..90b02a6aec5 100644
--- a/spec/javascripts/spec_helper.coffee
+++ b/spec/javascripts/spec_helper.coffee
@@ -9,6 +9,7 @@
# require the specific files that are being used in the spec that tests them.
#= require jquery
+#= require jquery.turbolinks
#= require bootstrap
#= require underscore
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
index 24894e81983..83e2ad220b8 100644
--- a/spec/lib/ci/charts_spec.rb
+++ b/spec/lib/ci/charts_spec.rb
@@ -4,13 +4,12 @@ describe "Charts" do
context "build_times" do
before do
- @project = FactoryGirl.create(:ci_project)
- @commit = FactoryGirl.create(:ci_commit, project: @project)
+ @commit = FactoryGirl.create(:ci_commit)
FactoryGirl.create(:ci_build, commit: @commit)
end
it 'should return build times in minutes' do
- chart = Ci::Charts::BuildTime.new(@project)
+ chart = Ci::Charts::BuildTime.new(@commit.project)
expect(chart.build_times).to eq([2])
end
end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 49482ac2b12..6f287719ba6 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -2,7 +2,8 @@ require 'spec_helper'
module Ci
describe GitlabCiYamlProcessor do
-
+ let(:path) { 'path' }
+
describe "#builds_for_ref" do
let(:type) { 'test' }
@@ -12,92 +13,234 @@ module Ci
rspec: { script: "rspec" }
})
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({
stage: "test",
+ stage_idx: 1,
except: nil,
name: :rspec,
only: nil,
- script: "pwd\nrspec",
- tags: [],
+ commands: "pwd\nrspec",
+ tag_list: [],
options: {},
- allow_failure: false
+ allow_failure: false,
+ when: "on_success"
})
end
+
+ describe :only do
+ it "does not return builds if only has another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["deploy"] }
+ })
- it "does not return builds if only has another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["deploy"] }
- })
+ config_processor = GitlabCiYamlProcessor.new(config, path)
- config_processor = GitlabCiYamlProcessor.new(config)
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ end
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
- end
+ it "does not return builds if only has regexp with another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["/^deploy$/"] }
+ })
- it "does not return builds if only has regexp with another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["/^deploy$/"] }
- })
+ config_processor = GitlabCiYamlProcessor.new(config, path)
- config_processor = GitlabCiYamlProcessor.new(config)
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ end
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
- end
+ it "returns builds if only has specified this branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["master"] }
+ })
- it "returns builds if only has specified this branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["master"] }
- })
+ config_processor = GitlabCiYamlProcessor.new(config, path)
- config_processor = GitlabCiYamlProcessor.new(config)
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ end
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
- end
+ it "returns builds if only has a list of branches including specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["master", "deploy"] }
+ })
- it "does not build tags" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["tags"] }
- })
+ config_processor = GitlabCiYamlProcessor.new(config, path)
- config_processor = GitlabCiYamlProcessor.new(config)
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
- expect(config_processor.builds_for_stage_and_ref(type, "0-1", true).size).to eq(0)
- end
+ it "returns builds if only has a branches keyword specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["branches"] }
+ })
- it "returns builds if only has a list of branches including specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["master", "deploy"] }
- })
+ config_processor = GitlabCiYamlProcessor.new(config, path)
- config_processor = GitlabCiYamlProcessor.new(config)
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "does not return builds if only has a tags keyword" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["tags"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "returns builds if only has current repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["branches@path"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ it "does not return builds if only has different repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["branches@fork"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "returns build only for specified type" do
+
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: "test", only: ["master", "deploy"] },
+ staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
+ production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] },
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, 'fork')
+
+ expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
+ expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1)
+ end
end
- it "returns build only for specified type" do
+ describe :except do
+ it "returns builds if except has another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["deploy"] }
+ })
- config = YAML.dump({
- before_script: ["pwd"],
- build: { script: "build", type: "build", only: ["master", "deploy"] },
- rspec: { script: "rspec", type: type, only: ["master", "deploy"] },
- staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
- production: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
- })
+ config_processor = GitlabCiYamlProcessor.new(config, path)
- config_processor = GitlabCiYamlProcessor.new(config)
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ end
+
+ it "returns builds if except has regexp with another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["/^deploy$/"] }
+ })
- expect(config_processor.builds_for_stage_and_ref("production", "deploy").size).to eq(0)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ end
+
+ it "does not return builds if except has specified this branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["master"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ end
+
+ it "does not return builds if except has a list of branches including specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["master", "deploy"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "does not return builds if except has a branches keyword specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["branches"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "returns builds if except has a tags keyword" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["tags"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "does not return builds if except has current repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["branches@path"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "returns builds if except has different repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["branches@fork"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "returns build except specified type" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] },
+ staging: { script: "deploy", type: "deploy", except: ["master"] },
+ production: { script: "deploy", type: "deploy", except: ["master@fork"] },
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, 'fork')
+
+ expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
+ expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0)
+ expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0)
+ end
end
+
end
describe "Image and service handling" do
@@ -109,21 +252,23 @@ module Ci
rspec: { script: "rspec" }
})
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
except: nil,
stage: "test",
+ stage_idx: 1,
name: :rspec,
only: nil,
- script: "pwd\nrspec",
- tags: [],
+ commands: "pwd\nrspec",
+ tag_list: [],
options: {
image: "ruby:2.1",
services: ["mysql"]
},
- allow_failure: false
+ allow_failure: false,
+ when: "on_success"
})
end
@@ -135,21 +280,23 @@ module Ci
rspec: { image: "ruby:2.5", services: ["postgresql"], script: "rspec" }
})
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
except: nil,
stage: "test",
+ stage_idx: 1,
name: :rspec,
only: nil,
- script: "pwd\nrspec",
- tags: [],
+ commands: "pwd\nrspec",
+ tag_list: [],
options: {
image: "ruby:2.5",
services: ["postgresql"]
},
- allow_failure: false
+ allow_failure: false,
+ when: "on_success"
})
end
end
@@ -166,148 +313,321 @@ module Ci
rspec: { script: "rspec" }
})
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.variables).to eq(variables)
end
end
+ describe "When" do
+ %w(on_success on_failure always).each do |when_state|
+ it "returns #{when_state} when defined" do
+ config = YAML.dump({
+ rspec: { script: "rspec", when: when_state }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+ builds = config_processor.builds_for_stage_and_ref("test", "master")
+ expect(builds.size).to eq(1)
+ expect(builds.first[:when]).to eq(when_state)
+ end
+ end
+ end
+
+ describe "Caches" do
+ it "returns cache when defined globally" do
+ config = YAML.dump({
+ cache: { paths: ["logs/", "binaries/"], untracked: true },
+ rspec: {
+ script: "rspec"
+ }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ )
+ end
+
+ it "returns cache when defined in a job" do
+ config = YAML.dump({
+ rspec: {
+ cache: { paths: ["logs/", "binaries/"], untracked: true },
+ script: "rspec"
+ }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ )
+ end
+
+ it "overwrite cache when defined for a job and globally" do
+ config = YAML.dump({
+ cache: { paths: ["logs/", "binaries/"], untracked: true },
+ rspec: {
+ script: "rspec",
+ cache: { paths: ["test/"], untracked: false },
+ }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
+ paths: ["test/"],
+ untracked: false,
+ )
+ end
+ end
+
+ describe "Artifacts" do
+ it "returns artifacts when defined" do
+ config = YAML.dump({
+ image: "ruby:2.1",
+ services: ["mysql"],
+ before_script: ["pwd"],
+ rspec: {
+ artifacts: { paths: ["logs/", "binaries/"], untracked: true },
+ script: "rspec"
+ }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ except: nil,
+ stage: "test",
+ stage_idx: 1,
+ name: :rspec,
+ only: nil,
+ commands: "pwd\nrspec",
+ tag_list: [],
+ options: {
+ image: "ruby:2.1",
+ services: ["mysql"],
+ artifacts: {
+ paths: ["logs/", "binaries/"],
+ untracked: true
+ }
+ },
+ when: "on_success",
+ allow_failure: false
+ })
+ end
+ end
+
describe "Error handling" do
+ it "fails to parse YAML" do
+ expect{GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
+ end
+
it "indicates that object is invalid" do
- expect{GitlabCiYamlProcessor.new("invalid_yaml\n!ccdvlf%612334@@@@")}.to raise_error(GitlabCiYamlProcessor::ValidationError)
+ expect{GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError)
end
it "returns errors if tags parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: tags parameter should be an array of strings")
end
it "returns errors if before_script parameter is invalid" do
config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script should be an array of strings")
end
it "returns errors if image parameter is invalid" do
config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image should be a string")
end
+ it "returns errors if job name is blank" do
+ config = YAML.dump({ '' => { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string")
+ end
+
+ it "returns errors if job name is non-string" do
+ config = YAML.dump({ 10 => { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string")
+ end
+
it "returns errors if job image parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: image should be a string")
end
it "returns errors if services parameter is not an array" do
config = YAML.dump({ services: "test", rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings")
end
it "returns errors if services parameter is not an array of strings" do
config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings")
end
it "returns errors if job services parameter is not an array" do
config = YAML.dump({ rspec: { script: "test", services: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings")
end
it "returns errors if job services parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings")
end
it "returns errors if there are unknown parameters" do
config = YAML.dump({ extra: "bundle update" })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
end
it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do
config = YAML.dump({ extra: { services: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
end
it "returns errors if there is no any jobs defined" do
config = YAML.dump({ before_script: ["bundle update"] })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Please define at least one job")
end
it "returns errors if job allow_failure parameter is not an boolean" do
config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: allow_failure parameter should be an boolean")
end
it "returns errors if job stage is not a string" do
config = YAML.dump({ rspec: { script: "test", type: 1, allow_failure: "string" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
end
it "returns errors if job stage is not a pre-defined stage" do
config = YAML.dump({ rspec: { script: "test", type: "acceptance", allow_failure: "string" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
end
it "returns errors if job stage is not a defined stage" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance", allow_failure: "string" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
end
it "returns errors if stages is not an array" do
config = YAML.dump({ types: "test", rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings")
end
it "returns errors if stages is not an array of strings" do
config = YAML.dump({ types: [true, "test"], rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings")
end
it "returns errors if variables is not a map" do
config = YAML.dump({ variables: "test", rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings")
end
it "returns errors if variables is not a map of key-valued strings" do
config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings")
end
+
+ it "returns errors if job when is not on_success, on_failure or always" do
+ config = YAML.dump({ rspec: { script: "test", when: 1 } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always")
+ end
+
+ it "returns errors if job artifacts:untracked is not an array of strings" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:untracked parameter should be an boolean")
+ end
+
+ it "returns errors if job artifacts:paths is not an array of strings" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { paths: "string" } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:paths parameter should be an array of strings")
+ end
+
+ it "returns errors if cache:untracked is not an array of strings" do
+ config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked parameter should be an boolean")
+ end
+
+ it "returns errors if cache:paths is not an array of strings" do
+ config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths parameter should be an array of strings")
+ end
+
+ it "returns errors if job cache:untracked is not an array of strings" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:untracked parameter should be an boolean")
+ end
+
+ it "returns errors if job cache:paths is not an array of strings" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { paths: "string" } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings")
+ end
end
end
end
diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb
index 829a9c197ef..dfa0e10318a 100644
--- a/spec/lib/gitlab/backend/grack_auth_spec.rb
+++ b/spec/lib/gitlab/backend/grack_auth_spec.rb
@@ -50,6 +50,22 @@ describe Grack::Auth do
end
end
+ context "when the Wiki for a project exists" do
+ before do
+ @wiki = ProjectWiki.new(project)
+ env["PATH_INFO"] = "#{@wiki.repository.path_with_namespace}.git/info/refs"
+ project.update_attribute(:visibility_level, Project::PUBLIC)
+ end
+
+ it "responds with the right project" do
+ response = auth.call(env)
+ json_body = ActiveSupport::JSON.decode(response[2][0])
+
+ expect(response.first).to eq(200)
+ expect(json_body['RepoPath']).to include(@wiki.repository.path_with_namespace)
+ end
+ end
+
context "when the project exists" do
before do
env["PATH_INFO"] = project.path_with_namespace + ".git"
@@ -151,14 +167,14 @@ describe Grack::Auth do
end
it "repeated attempts followed by successful attempt" do
- for n in 0..maxretry do
+ maxretry.times.each do
expect(attempt_login(false)).to eq(401)
end
expect(attempt_login(true)).to eq(200)
expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
- for n in 0..maxretry do
+ maxretry.times.each do
expect(attempt_login(false)).to eq(401)
end
end
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb
index b6d04330599..b60e23454d6 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/backend/shell_spec.rb
@@ -15,4 +15,17 @@ describe Gitlab::Shell do
it { is_expected.to respond_to :fork_repository }
it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") }
+
+ describe Gitlab::Shell::KeyAdder do
+ describe '#add_key' do
+ it 'normalizes space characters in the key' do
+ io = spy
+ adder = described_class.new(io)
+
+ adder.add_key('key-42', "sha-rsa foo\tbar\tbaz")
+
+ expect(io).to have_received(:puts).with("key-42\tsha-rsa foo bar baz")
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index 5d7ff4f6122..fe1b94a484e 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -2,11 +2,18 @@ require 'spec_helper'
describe Gitlab::ClosingIssueExtractor do
let(:project) { create(:project) }
+ let(:project2) { create(:project) }
let(:issue) { create(:issue, project: project) }
+ let(:issue2) { create(:issue, project: project2) }
let(:reference) { issue.to_reference }
+ let(:cross_reference) { issue2.to_reference(project) }
subject { described_class.new(project, project.creator) }
+ before do
+ project2.team << [project.creator, :master]
+ end
+
describe "#closed_by_message" do
context 'with a single reference' do
it do
@@ -130,6 +137,27 @@ describe Gitlab::ClosingIssueExtractor do
end
end
+ context "with a cross-project reference" do
+ it do
+ message = "Closes #{cross_reference}"
+ expect(subject.closed_by_message(message)).to eq([issue2])
+ end
+ end
+
+ context "with a cross-project URL" do
+ it do
+ message = "Closes #{urls.namespace_project_issue_url(issue2.project.namespace, issue2.project, issue2)}"
+ expect(subject.closed_by_message(message)).to eq([issue2])
+ end
+ end
+
+ context "with an invalid URL" do
+ it do
+ message = "Closes https://google.com#{urls.namespace_project_issue_path(issue2.project.namespace, issue2.project, issue2)}"
+ expect(subject.closed_by_message(message)).to eq([])
+ end
+ end
+
context 'with multiple references' do
let(:other_issue) { create(:issue, project: project) }
let(:third_issue) { create(:issue, project: project) }
@@ -140,28 +168,28 @@ describe Gitlab::ClosingIssueExtractor do
message = "Closes #{reference} and fix #{reference2}"
expect(subject.closed_by_message(message)).
- to eq([issue, other_issue])
+ to match_array([issue, other_issue])
end
it 'fetches comma-separated issues references in single line message' do
message = "Closes #{reference}, closes #{reference2}"
expect(subject.closed_by_message(message)).
- to eq([issue, other_issue])
+ to match_array([issue, other_issue])
end
it 'fetches comma-separated issues numbers in single line message' do
message = "Closes #{reference}, #{reference2} and #{reference3}"
expect(subject.closed_by_message(message)).
- to eq([issue, other_issue, third_issue])
+ to match_array([issue, other_issue, third_issue])
end
it 'fetches issues in multi-line message' do
message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}"
expect(subject.closed_by_message(message)).
- to eq([issue, other_issue])
+ to match_array([issue, other_issue])
end
it 'fetches issues in hybrid message' do
@@ -169,8 +197,33 @@ describe Gitlab::ClosingIssueExtractor do
"Also fixing issues #{reference2}, #{reference3} and #4"
expect(subject.closed_by_message(message)).
- to eq([issue, other_issue, third_issue])
+ to match_array([issue, other_issue, third_issue])
+ end
+
+ it "fetches cross-project references" do
+ message = "Closes #{reference} and #{cross_reference}"
+
+ expect(subject.closed_by_message(message)).
+ to match_array([issue, issue2])
+ end
+
+ it "fetches cross-project URL references" do
+ message = "Closes #{urls.namespace_project_issue_url(issue2.project.namespace, issue2.project, issue2)} and #{reference}"
+
+ expect(subject.closed_by_message(message)).
+ to match_array([issue, issue2])
+ end
+
+ it "ignores invalid cross-project URL references" do
+ message = "Closes https://google.com#{urls.namespace_project_issue_path(issue2.project.namespace, issue2.project, issue2)} and #{reference}"
+
+ expect(subject.closed_by_message(message)).
+ to match_array([issue])
end
end
end
+
+ def urls
+ Gitlab::Application.routes.url_helpers
+ end
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
new file mode 100644
index 00000000000..7cdebdf209a
--- /dev/null
+++ b/spec/lib/gitlab/database_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Gitlab::Database do
+ # These are just simple smoke tests to check if the methods work (regardless
+ # of what they may return).
+ describe '.mysql?' do
+ subject { described_class.mysql? }
+
+ it { is_expected.to satisfy { |val| val == true || val == false } }
+ end
+
+ describe '.postgresql?' do
+ subject { described_class.postgresql? }
+
+ it { is_expected.to satisfy { |val| val == true || val == false } }
+ end
+end
diff --git a/spec/lib/gitlab/email/attachment_uploader_spec.rb b/spec/lib/gitlab/email/attachment_uploader_spec.rb
index e8208e15e29..8fb432367b6 100644
--- a/spec/lib/gitlab/email/attachment_uploader_spec.rb
+++ b/spec/lib/gitlab/email/attachment_uploader_spec.rb
@@ -13,7 +13,6 @@ describe Gitlab::Email::AttachmentUploader do
expect(link).not_to be_nil
expect(link[:is_image]).to be_truthy
expect(link[:alt]).to eq("bricks")
- expect(link[:url]).to include("/#{project.path_with_namespace}")
expect(link[:url]).to include("bricks.png")
end
end
diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/inline_diff_spec.rb
index 2e0a05088cc..2e0a05088cc 100644
--- a/spec/lib/gitlab/diff/inline_diff_spec.rb
+++ b/spec/lib/gitlab/inline_diff_spec.rb
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index fd2e5f6d0e1..b5b56a34952 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -13,6 +13,17 @@ describe Gitlab::LDAP::User do
let(:auth_hash) do
OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info)
end
+ let(:ldap_user_upper_case) { Gitlab::LDAP::User.new(auth_hash_upper_case) }
+ let(:info_upper_case) do
+ {
+ name: 'John',
+ email: 'John@Example.com', # Email address has upper case chars
+ nickname: 'john'
+ }
+ end
+ let(:auth_hash_upper_case) do
+ OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info_upper_case)
+ end
describe :changed? do
it "marks existing ldap user as changed" do
@@ -57,6 +68,16 @@ describe Gitlab::LDAP::User do
expect(existing_user.id).to eql ldap_user.gl_user.id
end
+ it 'connects to existing ldap user if the extern_uid changes and email address has upper case characters' do
+ existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'old-uid', provider: 'ldapmain')
+ expect{ ldap_user_upper_case.save }.not_to change{ User.count }
+
+ existing_user.reload
+ expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid'
+ expect(existing_user.ldap_identity.provider).to eql 'ldapmain'
+ expect(existing_user.id).to eql ldap_user.gl_user.id
+ end
+
it 'maintains an identity per provider' do
existing_user = create(:omniauth_user, email: 'john@example.com', provider: 'twitter')
expect(existing_user.identities.count).to eql(1)
diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb
new file mode 100644
index 00000000000..5b13d65c7c0
--- /dev/null
+++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb
@@ -0,0 +1,765 @@
+require 'spec_helper'
+
+describe Gitlab::Lfs::Router do
+ let(:project) { create(:project) }
+ let(:public_project) { create(:project, :public) }
+ let(:forked_project) { fork_project(public_project, user) }
+
+ let(:user) { create(:user) }
+ let(:user_two) { create(:user) }
+ let!(:lfs_object) { create(:lfs_object, :with_file) }
+
+ let(:request) { Rack::Request.new(env) }
+ let(:env) do
+ {
+ 'rack.input' => '',
+ 'REQUEST_METHOD' => 'GET',
+ }
+ end
+
+ let(:lfs_router_auth) { new_lfs_router(project, user) }
+ let(:lfs_router_noauth) { new_lfs_router(project, nil) }
+ let(:lfs_router_public_auth) { new_lfs_router(public_project, user) }
+ let(:lfs_router_public_noauth) { new_lfs_router(public_project, nil) }
+ let(:lfs_router_forked_noauth) { new_lfs_router(forked_project, nil) }
+ let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user_two) }
+
+ let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" }
+ let(:sample_size) { 499013 }
+ let(:respond_with_deprecated) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]}
+ let(:respond_with_disabled) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]}
+
+ describe 'when lfs is disabled' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
+ env['REQUEST_METHOD'] = 'POST'
+ body = {
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ],
+ 'operation' => 'upload'
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch"
+ end
+
+ it 'responds with 501' do
+ expect(lfs_router_auth.try_call).to match_array(respond_with_disabled)
+ end
+ end
+
+ describe 'when fetching lfs object using deprecated API' do
+ before do
+ enable_lfs
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}"
+ end
+
+ it 'responds with 501' do
+ expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated)
+ end
+ end
+
+ describe 'when fetching lfs object' do
+ before do
+ enable_lfs
+ env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8"
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}"
+ end
+
+ describe 'and request comes from gitlab-workhorse' do
+ context 'without user being authorized' do
+ it "responds with status 401" do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'with required headers' do
+ before do
+ env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile"
+ end
+
+ context 'when user does not have project access' do
+ it "responds with status 403" do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+
+ context 'when user has project access' do
+ before do
+ project.lfs_objects << lfs_object
+ project.team << [user, :master]
+ end
+
+ it "responds with status 200" do
+ expect(lfs_router_auth.try_call.first).to eq(200)
+ end
+
+ it "responds with the file location" do
+ expect(lfs_router_auth.try_call[1]['Content-Type']).to eq("application/octet-stream")
+ expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
+ end
+ end
+ end
+
+ context 'without required headers' do
+ it "responds with status 403" do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+ end
+ end
+
+ describe 'when handling lfs request using deprecated API' do
+ before do
+ enable_lfs
+ env['REQUEST_METHOD'] = 'POST'
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects"
+ end
+
+ it 'responds with 501' do
+ expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated)
+ end
+ end
+
+ describe 'when handling lfs batch request' do
+ before do
+ enable_lfs
+ env['REQUEST_METHOD'] = 'POST'
+ env['PATH_INFO'] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch"
+ end
+
+ describe 'download' do
+ describe 'when user is authenticated' do
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+
+ describe 'when user has download access' do
+ before do
+ @auth = authorize(user)
+ env["HTTP_AUTHORIZATION"] = @auth
+ project.team << [user, :reporter]
+ end
+
+ context 'when downloading an lfs object that is assigned to our project' do
+ before do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200 and href to download' do
+ response = lfs_router_auth.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
+
+ expect(response_body).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => @auth }
+ }
+ }
+ }])
+ end
+ end
+
+ context 'when downloading an lfs object that is assigned to other project' do
+ before do
+ public_project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200 and error message' do
+ response = lfs_router_auth.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
+
+ expect(response_body).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ }])
+ end
+ end
+
+ context 'when downloading a lfs object that does not exist' do
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+
+ it "responds with status 200 and error message" do
+ response = lfs_router_auth.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
+
+ expect(response_body).to eq('objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ }])
+ end
+ end
+
+ context 'when downloading one new and one existing lfs object' do
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ project.lfs_objects << lfs_object
+ end
+
+ it "responds with status 200 with upload hypermedia link for the new object" do
+ response = lfs_router_auth.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
+
+ expect(response_body).to eq('objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => @auth }
+ }
+ }
+ }])
+ end
+ end
+ end
+
+ context 'when user does is not member of the project' do
+ before do
+ @auth = authorize(user)
+ env["HTTP_AUTHORIZATION"] = @auth
+ project.team << [user, :guest]
+ end
+
+ it 'responds with 403' do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+
+ context 'when user does not have download access' do
+ before do
+ @auth = authorize(user)
+ env["HTTP_AUTHORIZATION"] = @auth
+ project.team << [user, :guest]
+ end
+
+ it 'responds with 403' do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+ end
+
+ context 'when user is not authenticated' do
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }],
+
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+
+ describe 'is accessing public project' do
+ before do
+ public_project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200 and href to download' do
+ response = lfs_router_public_noauth.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
+
+ expect(response_body).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{public_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => {}
+ }
+ }
+ }])
+ end
+ end
+
+ describe 'is accessing non-public project' do
+ before do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with authorization required' do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ end
+
+ describe 'upload' do
+ describe 'when user is authenticated' do
+ before do
+ body = { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+
+ describe 'when user has project push access' do
+ before do
+ @auth = authorize(user)
+ env["HTTP_AUTHORIZATION"] = @auth
+ project.team << [user, :developer]
+ end
+
+ context 'when pushing an lfs object that already exists' do
+ before do
+ public_project.lfs_objects << lfs_object
+ end
+
+ it "responds with status 200 and links the object to the project" do
+ response_body = lfs_router_auth.try_call.last
+ response = ActiveSupport::JSON.decode(response_body.first)
+
+ expect(response['objects']).to be_kind_of(Array)
+ expect(response['objects'].first['oid']).to eq(sample_oid)
+ expect(response['objects'].first['size']).to eq(sample_size)
+ expect(lfs_object.projects.pluck(:id)).to_not include(project.id)
+ expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
+ expect(response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
+ expect(response['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth)
+ end
+ end
+
+ context 'when pushing a lfs object that does not exist' do
+ before do
+ body = { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+
+ it "responds with status 200 and upload hypermedia link" do
+ response = lfs_router_auth.try_call
+ expect(response.first).to eq(200)
+
+ response_body = ActiveSupport::JSON.decode(response.last.first)
+ expect(response_body['objects']).to be_kind_of(Array)
+ expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
+ expect(response_body['objects'].first['size']).to eq(1575078)
+ expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
+ expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
+ expect(response_body['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth)
+ end
+ end
+
+ context 'when pushing one new and one existing lfs object' do
+ before do
+ body = { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ project.lfs_objects << lfs_object
+ end
+
+ it "responds with status 200 with upload hypermedia link for the new object" do
+ response = lfs_router_auth.try_call
+ expect(response.first).to eq(200)
+
+ response_body = ActiveSupport::JSON.decode(response.last.first)
+ expect(response_body['objects']).to be_kind_of(Array)
+
+ expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
+ expect(response_body['objects'].first['size']).to eq(1575078)
+ expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
+ expect(response_body['objects'].first['actions']['upload']['header']).to eq("Authorization" => @auth)
+
+ expect(response_body['objects'].last['oid']).to eq(sample_oid)
+ expect(response_body['objects'].last['size']).to eq(sample_size)
+ expect(response_body['objects'].last).to_not have_key('actions')
+ end
+ end
+ end
+
+ context 'when user does not have push access' do
+ it 'responds with 403' do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+ end
+
+ context 'when user is not authenticated' do
+ before do
+ env['rack.input'] = StringIO.new(
+ { 'objects' => [], 'operation' => 'upload' }.to_json
+ )
+ end
+
+ context 'when user has push access' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it "responds with status 401" do
+ expect(lfs_router_public_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'when user does not have push access' do
+ it "responds with status 401" do
+ expect(lfs_router_public_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ end
+
+ describe 'unsupported' do
+ before do
+ body = { 'operation' => 'other',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+
+ it 'responds with status 404' do
+ expect(lfs_router_public_noauth.try_call.first).to eq(404)
+ end
+ end
+ end
+
+ describe 'when pushing a lfs object' do
+ before do
+ enable_lfs
+ env['REQUEST_METHOD'] = 'PUT'
+ end
+
+ describe 'to one project' do
+ describe 'when user has push access to the project' do
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'when user is authenticated' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(project)
+ end
+
+ it 'responds with status 200, location of lfs store and object details' do
+ json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first)
+
+ expect(lfs_router_auth.try_call.first).to eq(200)
+ expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(project)
+ end
+
+ it 'responds with status 200 and lfs object is linked to the project' do
+ expect(lfs_router_auth.try_call.first).to eq(200)
+ expect(lfs_object.projects.pluck(:id)).to include(project.id)
+ end
+ end
+ end
+
+ describe 'when user is unauthenticated' do
+ let(:lfs_router_noauth) { new_lfs_router(project, nil) }
+
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(project)
+ end
+
+ it 'responds with status 401' do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(project)
+ end
+
+ it 'responds with status 401' do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent with a malformed headers' do
+ before do
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
+ env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd"
+ end
+
+ it 'does not recognize it as a valid lfs command' do
+ expect(lfs_router_noauth.try_call).to eq(nil)
+ end
+ end
+ end
+ end
+
+ describe 'and user does not have push access' do
+ describe 'when user is authenticated' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(project)
+ end
+
+ it 'responds with 403' do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(project)
+ end
+
+ it 'responds with 403' do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+ end
+
+ describe 'when user is unauthenticated' do
+ let(:lfs_router_noauth) { new_lfs_router(project, nil) }
+
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(project)
+ end
+
+ it 'responds with 401' do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(project)
+ end
+
+ it 'responds with 401' do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ end
+ end
+
+ describe "to a forked project" do
+ let(:forked_project) { fork_project(public_project, user) }
+
+ describe 'when user has push access to the project' do
+ before do
+ forked_project.team << [user_two, :master]
+ end
+
+ describe 'when user is authenticated' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(forked_project)
+ end
+
+ it 'responds with status 200, location of lfs store and object details' do
+ json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first)
+
+ expect(lfs_router_forked_auth.try_call.first).to eq(200)
+ expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(forked_project)
+ end
+
+ it 'responds with status 200 and lfs object is linked to the source project' do
+ expect(lfs_router_forked_auth.try_call.first).to eq(200)
+ expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
+ end
+ end
+ end
+
+ describe 'when user is unauthenticated' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(forked_project)
+ end
+
+ it 'responds with status 401' do
+ expect(lfs_router_forked_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(forked_project)
+ end
+
+ it 'responds with status 401' do
+ expect(lfs_router_forked_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ end
+
+ describe 'and user does not have push access' do
+ describe 'when user is authenticated' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(forked_project)
+ end
+
+ it 'responds with 403' do
+ expect(lfs_router_forked_auth.try_call.first).to eq(403)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(forked_project)
+ end
+
+ it 'responds with 403' do
+ expect(lfs_router_forked_auth.try_call.first).to eq(403)
+ end
+ end
+ end
+
+ describe 'when user is unauthenticated' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(forked_project)
+ end
+
+ it 'responds with 401' do
+ expect(lfs_router_forked_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(forked_project)
+ end
+
+ it 'responds with 401' do
+ expect(lfs_router_forked_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ end
+
+ describe 'and second project not related to fork or a source project' do
+ let(:second_project) { create(:project) }
+ let(:lfs_router_second_project) { new_lfs_router(second_project, user) }
+
+ before do
+ public_project.lfs_objects << lfs_object
+ headers_for_upload_finalize(second_project)
+ end
+
+ context 'when pushing the same lfs object to the second project' do
+ before do
+ second_project.team << [user, :master]
+ end
+
+ it 'responds with 200 and links the lfs object to the project' do
+ expect(lfs_router_second_project.try_call.first).to eq(200)
+ expect(lfs_object.projects.pluck(:id)).to include(second_project.id, public_project.id)
+ end
+ end
+ end
+ end
+ end
+
+ def enable_lfs
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ def authorize(user)
+ ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
+ end
+
+ def new_lfs_router(project, user)
+ Gitlab::Lfs::Router.new(project, user, request)
+ end
+
+ def header_for_upload_authorize(project)
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize"
+ end
+
+ def headers_for_upload_finalize(project)
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
+ env["HTTP_X_GITLAB_LFS_TMP"] = "#{sample_oid}6e561c9d4"
+ end
+
+ def fork_project(project, user, object = nil)
+ allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
+ Projects::ForkService.new(project, user, {}).execute
+ end
+end
diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb
index 3c6c84a0416..9ce63f9af46 100644
--- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb
+++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb
@@ -4,12 +4,12 @@ module Gitlab::Markdown
describe CommitRangeReferenceFilter do
include FilterSpecHelper
- let(:project) { create(:project) }
- let(:commit1) { project.commit }
- let(:commit2) { project.commit("HEAD~2") }
+ let(:project) { create(:project, :public) }
+ let(:commit1) { project.commit("HEAD~2") }
+ let(:commit2) { project.commit }
- let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}") }
- let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}") }
+ let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}", project) }
+ let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}", project) }
it 'requires project context' do
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
@@ -18,7 +18,7 @@ module Gitlab::Markdown
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Commit Range #{range.to_reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
end
@@ -27,14 +27,14 @@ module Gitlab::Markdown
let(:reference2) { range2.to_reference }
it 'links to a valid two-dot reference' do
- doc = filter("See #{reference2}")
+ doc = reference_filter("See #{reference2}")
expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param)
end
it 'links to a valid three-dot reference' do
- doc = filter("See #{reference}")
+ doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param)
@@ -46,14 +46,14 @@ module Gitlab::Markdown
exp = commit1.short_id + '...' + commit2.short_id
- expect(filter("See #{reference}").css('a').first.text).to eq exp
- expect(filter("See #{reference2}").css('a').first.text).to eq exp
+ expect(reference_filter("See #{reference}").css('a').first.text).to eq exp
+ expect(reference_filter("See #{reference2}").css('a').first.text).to eq exp
end
it 'links with adjacent text' do
- doc = filter("See (#{reference}.)")
+ doc = reference_filter("See (#{reference}.)")
- exp = Regexp.escape(range.to_s)
+ exp = Regexp.escape(range.reference_link_text)
expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
end
@@ -62,29 +62,38 @@ module Gitlab::Markdown
expect(project).to receive(:valid_repo?).and_return(true)
expect(project.repository).to receive(:commit).with(commit1.id.reverse)
- expect(filter(act).to_html).to eq exp
+ expect(project.repository).to receive(:commit).with(commit2.id)
+ expect(reference_filter(act).to_html).to eq exp
end
it 'includes a title attribute' do
- doc = filter("See #{reference}")
+ doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('title')).to eq range.reference_title
end
it 'includes default classes' do
- doc = filter("See #{reference}")
+ doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
end
- it 'includes a data-project-id attribute' do
- doc = filter("See #{reference}")
+ it 'includes a data-project attribute' do
+ doc = reference_filter("See #{reference}")
link = doc.css('a').first
- expect(link).to have_attribute('data-project-id')
- expect(link.attr('data-project-id')).to eq project.id.to_s
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-commit-range attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-commit-range')
+ expect(link.attr('data-commit-range')).to eq range.to_s
end
it 'supports an :only_path option' do
- doc = filter("See #{reference}", only_path: true)
+ doc = reference_filter("See #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
@@ -92,59 +101,83 @@ module Gitlab::Markdown
end
it 'adds to the results hash' do
- result = pipeline_result("See #{reference}")
+ result = reference_pipeline_result("See #{reference}")
expect(result[:references][:commit_range]).not_to be_empty
end
end
context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:project, namespace: namespace) }
+ let(:project2) { create(:project, :public, namespace: namespace) }
let(:reference) { range.to_reference(project) }
before do
range.project = project2
end
- context 'when user can access reference' do
- before { allow_cross_reference! }
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}")
+ expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid commit IDs on the referenced project' do
+ exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
+ expect(reference_filter(act).to_html).to eq exp
+
+ exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
+ expect(reference_filter(act).to_html).to eq exp
+ end
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("See #{reference}")
+ expect(result[:references][:commit_range]).not_to be_empty
+ end
+ end
- expect(doc.css('a').first.attr('href')).
- to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param)
- end
+ context 'cross-project URL reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:range) { CommitRange.new("#{commit1.id}...master", project) }
+ let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') }
- it 'links with adjacent text' do
- doc = filter("Fixed (#{reference}.)")
+ before do
+ range.project = project2
+ end
- exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}")
- expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
- end
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
- it 'ignores invalid commit IDs on the referenced project' do
- exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
- expect(filter(act).to_html).to eq exp
+ expect(doc.css('a').first.attr('href')).
+ to eq reference
+ end
- exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
- expect(filter(act).to_html).to eq exp
- end
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
- it 'adds to the results hash' do
- result = pipeline_result("See #{reference}")
- expect(result[:references][:commit_range]).not_to be_empty
- end
+ exp = Regexp.escape(range.reference_link_text(project))
+ expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
end
- context 'when user cannot access reference' do
- before { disallow_cross_reference! }
+ it 'ignores invalid commit IDs on the referenced project' do
+ exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
+ expect(reference_filter(act).to_html).to eq exp
- it 'ignores valid references' do
- exp = act = "See #{reference}"
+ exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
+ expect(reference_filter(act).to_html).to eq exp
+ end
- expect(filter(act).to_html).to eq exp
- end
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("See #{reference}")
+ expect(result[:references][:commit_range]).not_to be_empty
end
end
end
diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb
index 9ed438252b3..462a41b4756 100644
--- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb
+++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb
@@ -4,7 +4,7 @@ module Gitlab::Markdown
describe CommitReferenceFilter do
include FilterSpecHelper
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
let(:commit) { project.commit }
it 'requires project context' do
@@ -14,7 +14,7 @@ module Gitlab::Markdown
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
end
@@ -24,7 +24,7 @@ module Gitlab::Markdown
# Let's test a variety of commit SHA sizes just to be paranoid
[6, 8, 12, 18, 20, 32, 40].each do |size|
it "links to a valid reference of #{size} characters" do
- doc = filter("See #{reference[0...size]}")
+ doc = reference_filter("See #{reference[0...size]}")
expect(doc.css('a').first.text).to eq commit.short_id
expect(doc.css('a').first.attr('href')).
@@ -33,15 +33,15 @@ module Gitlab::Markdown
end
it 'always uses the short ID as the link text' do
- doc = filter("See #{commit.id}")
+ doc = reference_filter("See #{commit.id}")
expect(doc.text).to eq "See #{commit.short_id}"
- doc = filter("See #{commit.id[0...6]}")
+ doc = reference_filter("See #{commit.id[0...6]}")
expect(doc.text).to eq "See #{commit.short_id}"
end
it 'links with adjacent text' do
- doc = filter("See (#{reference}.)")
+ doc = reference_filter("See (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/)
end
@@ -51,36 +51,44 @@ module Gitlab::Markdown
expect(project).to receive(:valid_repo?).and_return(true)
expect(project.repository).to receive(:commit).with(invalid)
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
it 'includes a title attribute' do
- doc = filter("See #{reference}")
+ doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('title')).to eq commit.link_title
end
it 'escapes the title attribute' do
allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="})
- doc = filter("See #{reference}")
+ doc = reference_filter("See #{reference}")
expect(doc.text).to eq "See #{commit.short_id}"
end
it 'includes default classes' do
- doc = filter("See #{reference}")
+ doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
end
- it 'includes a data-project-id attribute' do
- doc = filter("See #{reference}")
+ it 'includes a data-project attribute' do
+ doc = reference_filter("See #{reference}")
link = doc.css('a').first
- expect(link).to have_attribute('data-project-id')
- expect(link.attr('data-project-id')).to eq project.id.to_s
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-commit attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-commit')
+ expect(link.attr('data-commit')).to eq commit.id
end
it 'supports an :only_path context' do
- doc = filter("See #{reference}", only_path: true)
+ doc = reference_filter("See #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
@@ -88,53 +96,69 @@ module Gitlab::Markdown
end
it 'adds to the results hash' do
- result = pipeline_result("See #{reference}")
+ result = reference_pipeline_result("See #{reference}")
expect(result[:references][:commit]).not_to be_empty
end
end
context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:project, namespace: namespace) }
+ let(:project2) { create(:project, :public, namespace: namespace) }
let(:commit) { project2.commit }
let(:reference) { commit.to_reference(project) }
- context 'when user can access reference' do
- before { allow_cross_reference! }
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
+ end
- expect(doc.css('a').first.attr('href')).
- to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
- end
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
- it 'links with adjacent text' do
- doc = filter("Fixed (#{reference}.)")
+ exp = Regexp.escape(project2.to_reference)
+ expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/)
+ end
- exp = Regexp.escape(project2.to_reference)
- expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/)
- end
+ it 'ignores invalid commit IDs on the referenced project' do
+ exp = act = "Committed #{invalidate_reference(reference)}"
+ expect(reference_filter(act).to_html).to eq exp
+ end
- it 'ignores invalid commit IDs on the referenced project' do
- exp = act = "Committed #{invalidate_reference(reference)}"
- expect(filter(act).to_html).to eq exp
- end
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("See #{reference}")
+ expect(result[:references][:commit]).not_to be_empty
+ end
+ end
- it 'adds to the results hash' do
- result = pipeline_result("See #{reference}")
- expect(result[:references][:commit]).not_to be_empty
- end
+ context 'cross-project URL reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:commit) { project2.commit }
+ let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
end
- context 'when user cannot access reference' do
- before { disallow_cross_reference! }
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
- it 'ignores valid references' do
- exp = act = "See #{reference}"
+ expect(doc.to_html).to match(/\(<a.+>#{commit.reference_link_text(project)}<\/a>\.\)/)
+ end
- expect(filter(act).to_html).to eq exp
- end
+ it 'ignores invalid commit IDs on the referenced project' do
+ act = "Committed #{invalidate_reference(reference)}"
+ expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
+ end
+
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("See #{reference}")
+ expect(result[:references][:commit]).not_to be_empty
end
end
end
diff --git a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb
index 4698d6138c2..8d4f9e403a6 100644
--- a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb
+++ b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb
@@ -2,20 +2,16 @@ require 'spec_helper'
module Gitlab::Markdown
describe CrossProjectReference do
- # context in the html-pipeline sense, not in the rspec sense
- let(:context) do
- {
- current_user: double('user'),
- project: double('project')
- }
- end
-
include described_class
describe '#project_from_ref' do
context 'when no project was referenced' do
it 'returns the project from context' do
- expect(project_from_ref(nil)).to eq context[:project]
+ project = double
+
+ allow(self).to receive(:context).and_return({ project: project })
+
+ expect(project_from_ref(nil)).to eq project
end
end
@@ -26,29 +22,13 @@ module Gitlab::Markdown
end
context 'when referenced project exists' do
- let(:project2) { double('referenced project') }
+ it 'returns the referenced project' do
+ project2 = double('referenced project')
- before do
expect(Project).to receive(:find_with_namespace).
with('cross/reference').and_return(project2)
- end
-
- context 'and the user has permission to read it' do
- it 'returns the referenced project' do
- expect(self).to receive(:user_can_reference_project?).
- with(project2).and_return(true)
-
- expect(project_from_ref('cross/reference')).to eq project2
- end
- end
-
- context 'and the user does not have permission to read it' do
- it 'returns nil' do
- expect(self).to receive(:user_can_reference_project?).
- with(project2).and_return(false)
- expect(project_from_ref('cross/reference')).to be_nil
- end
+ expect(project_from_ref('cross/reference')).to eq project2
end
end
end
diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb
index 1dd54f58588..078ff3ed4b2 100644
--- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb
+++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb
@@ -8,7 +8,7 @@ module Gitlab::Markdown
IssuesHelper
end
- let(:project) { create(:empty_project) }
+ let(:project) { create(:empty_project, :public) }
let(:issue) { create(:issue, project: project) }
it 'requires project context' do
@@ -18,7 +18,7 @@ module Gitlab::Markdown
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Issue #{issue.to_reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
end
@@ -29,18 +29,18 @@ module Gitlab::Markdown
expect(project).to receive(:get_issue).with(issue.iid).and_return(nil)
exp = act = "Issue #{reference}"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
it 'links to a valid reference' do
- doc = filter("Fixed #{reference}")
+ doc = reference_filter("Fixed #{reference}")
expect(doc.css('a').first.attr('href')).
to eq helper.url_for_issue(issue.iid, project)
end
it 'links with adjacent text' do
- doc = filter("Fixed (#{reference}.)")
+ doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
@@ -48,36 +48,44 @@ module Gitlab::Markdown
invalid = invalidate_reference(reference)
exp = act = "Fixed #{invalid}"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
it 'includes a title attribute' do
- doc = filter("Issue #{reference}")
+ doc = reference_filter("Issue #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}"
end
it 'escapes the title attribute' do
issue.update_attribute(:title, %{"></a>whatever<a title="})
- doc = filter("Issue #{reference}")
+ doc = reference_filter("Issue #{reference}")
expect(doc.text).to eq "Issue #{reference}"
end
it 'includes default classes' do
- doc = filter("Issue #{reference}")
+ doc = reference_filter("Issue #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
end
- it 'includes a data-project-id attribute' do
- doc = filter("Issue #{reference}")
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Issue #{reference}")
link = doc.css('a').first
- expect(link).to have_attribute('data-project-id')
- expect(link.attr('data-project-id')).to eq project.id.to_s
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-issue attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-issue')
+ expect(link.attr('data-issue')).to eq issue.id.to_s
end
it 'supports an :only_path context' do
- doc = filter("Issue #{reference}", only_path: true)
+ doc = reference_filter("Issue #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
@@ -85,60 +93,118 @@ module Gitlab::Markdown
end
it 'adds to the results hash' do
- result = pipeline_result("Fixed #{reference}")
+ result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
end
context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:empty_project, namespace: namespace) }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:issue) { create(:issue, project: project2) }
let(:reference) { issue.to_reference(project) }
- context 'when user can access reference' do
- before { allow_cross_reference! }
+ it 'ignores valid references when cross-reference project uses external tracker' do
+ expect_any_instance_of(Project).to receive(:get_issue).
+ with(issue.iid).and_return(nil)
+
+ exp = act = "Issue #{reference}"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project2)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid issue IDs on the referenced project' do
+ exp = act = "Fixed #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("Fixed #{reference}")
+ expect(result[:references][:issue]).to eq [issue]
+ end
+ end
+
+ context 'cross-project URL reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let(:issue) { create(:issue, project: project2) }
+ let(:reference) { helper.url_for_issue(issue.iid, project2) + "#note_123" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
- it 'ignores valid references when cross-reference project uses external tracker' do
- expect_any_instance_of(Project).to receive(:get_issue).
- with(issue.iid).and_return(nil)
+ expect(doc.css('a').first.attr('href')).
+ to eq reference
+ end
- exp = act = "Issue #{reference}"
- expect(filter(act).to_html).to eq exp
- end
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/)
+ end
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("Fixed #{reference}")
+ expect(result[:references][:issue]).to eq [issue]
+ end
+ end
- expect(doc.css('a').first.attr('href')).
- to eq helper.url_for_issue(issue.iid, project2)
- end
+ context 'cross-project reference in link href' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let(:issue) { create(:issue, project: project2) }
+ let(:reference) { %Q{<a href="#{issue.to_reference(project)}">Reference</a>} }
- it 'links with adjacent text' do
- doc = filter("Fixed (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
- end
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
- it 'ignores invalid issue IDs on the referenced project' do
- exp = act = "Fixed #{invalidate_reference(reference)}"
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project2)
+ end
- expect(filter(act).to_html).to eq exp
- end
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
+ end
- it 'adds to the results hash' do
- result = pipeline_result("Fixed #{reference}")
- expect(result[:references][:issue]).to eq [issue]
- end
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("Fixed #{reference}")
+ expect(result[:references][:issue]).to eq [issue]
end
+ end
+
+ context 'cross-project URL in link href' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let(:issue) { create(:issue, project: project2) }
+ let(:reference) { %Q{<a href="#{helper.url_for_issue(issue.iid, project2) + "#note_123"}">Reference</a>} }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
- context 'when user cannot access reference' do
- before { disallow_cross_reference! }
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project2) + "#note_123"
+ end
- it 'ignores valid references' do
- exp = act = "See #{reference}"
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
+ end
- expect(filter(act).to_html).to eq exp
- end
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("Fixed #{reference}")
+ expect(result[:references][:issue]).to eq [issue]
end
end
end
diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb
index e32089de376..ef6dd524aba 100644
--- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb
+++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb
@@ -5,7 +5,7 @@ module Gitlab::Markdown
describe LabelReferenceFilter do
include FilterSpecHelper
- let(:project) { create(:empty_project) }
+ let(:project) { create(:empty_project, :public) }
let(:label) { create(:label, project: project) }
let(:reference) { label.to_reference }
@@ -16,25 +16,33 @@ module Gitlab::Markdown
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Label #{reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
end
it 'includes default classes' do
- doc = filter("Label #{reference}")
+ doc = reference_filter("Label #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
end
- it 'includes a data-project-id attribute' do
- doc = filter("Label #{reference}")
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Label #{reference}")
link = doc.css('a').first
- expect(link).to have_attribute('data-project-id')
- expect(link.attr('data-project-id')).to eq project.id.to_s
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-label attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-label')
+ expect(link.attr('data-label')).to eq label.id.to_s
end
it 'supports an :only_path context' do
- doc = filter("Label #{reference}", only_path: true)
+ doc = reference_filter("Label #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
@@ -42,39 +50,39 @@ module Gitlab::Markdown
end
it 'adds to the results hash' do
- result = pipeline_result("Label #{reference}")
+ result = reference_pipeline_result("Label #{reference}")
expect(result[:references][:label]).to eq [label]
end
describe 'label span element' do
it 'includes default classes' do
- doc = filter("Label #{reference}")
+ doc = reference_filter("Label #{reference}")
expect(doc.css('a span').first.attr('class')).to eq 'label color-label'
end
it 'includes a style attribute' do
- doc = filter("Label #{reference}")
+ doc = reference_filter("Label #{reference}")
expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/)
end
end
context 'Integer-based references' do
it 'links to a valid reference' do
- doc = filter("See #{reference}")
+ doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
end
it 'links with adjacent text' do
- doc = filter("Label (#{reference}.)")
+ doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
end
it 'ignores invalid label IDs' do
exp = act = "Label #{invalidate_reference(reference)}"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
end
@@ -83,7 +91,7 @@ module Gitlab::Markdown
let(:reference) { "#{Label.reference_prefix}#{label.name}" }
it 'links to a valid reference' do
- doc = filter("See #{reference}")
+ doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
@@ -91,14 +99,14 @@ module Gitlab::Markdown
end
it 'links with adjacent text' do
- doc = filter("Label (#{reference}.)")
+ doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
end
it 'ignores invalid label names' do
exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
end
@@ -107,7 +115,7 @@ module Gitlab::Markdown
let(:reference) { label.to_reference(:name) }
it 'links to a valid reference' do
- doc = filter("See #{reference}")
+ doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
@@ -115,21 +123,58 @@ module Gitlab::Markdown
end
it 'links with adjacent text' do
- doc = filter("Label (#{reference}.)")
+ doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
end
it 'ignores invalid label names' do
exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}")
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
end
describe 'edge cases' do
it 'gracefully handles non-references matching the pattern' do
exp = act = '(format nil "~0f" 3.0) ; 3.0'
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ describe 'referencing a label in a link href' do
+ let(:reference) { %Q{<a href="#{label.to_reference}">Label</a>} }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: label.name)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+>Label</a>\.\)))
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Label #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-label attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-label')
+ expect(link.attr('data-label')).to eq label.id.to_s
+ end
+
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("Label #{reference}")
+ expect(result[:references][:label]).to eq [label]
end
end
end
diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb
index 66616b93368..4a232051127 100644
--- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb
+++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb
@@ -4,7 +4,7 @@ module Gitlab::Markdown
describe MergeRequestReferenceFilter do
include FilterSpecHelper
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
let(:merge) { create(:merge_request, source_project: project) }
it 'requires project context' do
@@ -14,7 +14,7 @@ module Gitlab::Markdown
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Merge #{merge.to_reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
end
@@ -22,50 +22,58 @@ module Gitlab::Markdown
let(:reference) { merge.to_reference }
it 'links to a valid reference' do
- doc = filter("See #{reference}")
+ doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_merge_request_url(project.namespace, project, merge)
end
it 'links with adjacent text' do
- doc = filter("Merge (#{reference}.)")
+ doc = reference_filter("Merge (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid merge IDs' do
exp = act = "Merge #{invalidate_reference(reference)}"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
it 'includes a title attribute' do
- doc = filter("Merge #{reference}")
+ doc = reference_filter("Merge #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
end
it 'escapes the title attribute' do
merge.update_attribute(:title, %{"></a>whatever<a title="})
- doc = filter("Merge #{reference}")
+ doc = reference_filter("Merge #{reference}")
expect(doc.text).to eq "Merge #{reference}"
end
it 'includes default classes' do
- doc = filter("Merge #{reference}")
+ doc = reference_filter("Merge #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
end
- it 'includes a data-project-id attribute' do
- doc = filter("Merge #{reference}")
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Merge #{reference}")
link = doc.css('a').first
- expect(link).to have_attribute('data-project-id')
- expect(link.attr('data-project-id')).to eq project.id.to_s
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-merge-request attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-merge-request')
+ expect(link.attr('data-merge-request')).to eq merge.id.to_s
end
it 'supports an :only_path context' do
- doc = filter("Merge #{reference}", only_path: true)
+ doc = reference_filter("Merge #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
@@ -73,53 +81,63 @@ module Gitlab::Markdown
end
it 'adds to the results hash' do
- result = pipeline_result("Merge #{reference}")
+ result = reference_pipeline_result("Merge #{reference}")
expect(result[:references][:merge_request]).to eq [merge]
end
end
context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:project, namespace: namespace) }
- let(:merge) { create(:merge_request, source_project: project2) }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:merge) { create(:merge_request, source_project: project2, target_project: project2) }
let(:reference) { merge.to_reference(project) }
- context 'when user can access reference' do
- before { allow_cross_reference! }
-
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
- expect(doc.css('a').first.attr('href')).
- to eq urls.namespace_project_merge_request_url(project2.namespace,
- project, merge)
- end
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_merge_request_url(project2.namespace,
+ project2, merge)
+ end
- it 'links with adjacent text' do
- doc = filter("Merge (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
- end
+ it 'links with adjacent text' do
+ doc = reference_filter("Merge (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
- it 'ignores invalid merge IDs on the referenced project' do
- exp = act = "Merge #{invalidate_reference(reference)}"
+ it 'ignores invalid merge IDs on the referenced project' do
+ exp = act = "Merge #{invalidate_reference(reference)}"
- expect(filter(act).to_html).to eq exp
- end
+ expect(reference_filter(act).to_html).to eq exp
+ end
- it 'adds to the results hash' do
- result = pipeline_result("Merge #{reference}")
- expect(result[:references][:merge_request]).to eq [merge]
- end
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("Merge #{reference}")
+ expect(result[:references][:merge_request]).to eq [merge]
end
+ end
+
+ context 'cross-project URL reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:merge) { create(:merge_request, source_project: project2, target_project: project2) }
+ let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, project2, merge) + '/diffs#note_123' }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
- context 'when user cannot access reference' do
- before { disallow_cross_reference! }
+ expect(doc.css('a').first.attr('href')).
+ to eq reference
+ end
- it 'ignores valid references' do
- exp = act = "See #{reference}"
+ it 'links with adjacent text' do
+ doc = reference_filter("Merge (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
+ end
- expect(filter(act).to_html).to eq exp
- end
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("Merge #{reference}")
+ expect(result[:references][:merge_request]).to eq [merge]
end
end
end
diff --git a/spec/lib/gitlab/markdown/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/redactor_filter_spec.rb
new file mode 100644
index 00000000000..eea3f1cf370
--- /dev/null
+++ b/spec/lib/gitlab/markdown/redactor_filter_spec.rb
@@ -0,0 +1,91 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe RedactorFilter do
+ include ActionView::Helpers::UrlHelper
+ include FilterSpecHelper
+
+ it 'ignores non-GFM links' do
+ html = %(See <a href="https://google.com/">Google</a>)
+ doc = filter(html, current_user: double)
+
+ expect(doc.css('a').length).to eq 1
+ end
+
+ def reference_link(data)
+ link_to('text', '', class: 'gfm', data: data)
+ end
+
+ context 'with data-project' do
+ it 'removes unpermitted Project references' do
+ user = create(:user)
+ project = create(:empty_project)
+
+ link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name)
+ doc = filter(link, current_user: user)
+
+ expect(doc.css('a').length).to eq 0
+ end
+
+ it 'allows permitted Project references' do
+ user = create(:user)
+ project = create(:empty_project)
+ project.team << [user, :master]
+
+ link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name)
+ doc = filter(link, current_user: user)
+
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'handles invalid Project references' do
+ link = reference_link(project: 12345, reference_filter: Gitlab::Markdown::ReferenceFilter.name)
+
+ expect { filter(link) }.not_to raise_error
+ end
+ end
+
+ context "for user references" do
+
+ context 'with data-group' do
+ it 'removes unpermitted Group references' do
+ user = create(:user)
+ group = create(:group)
+
+ link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
+ doc = filter(link, current_user: user)
+
+ expect(doc.css('a').length).to eq 0
+ end
+
+ it 'allows permitted Group references' do
+ user = create(:user)
+ group = create(:group)
+ group.add_developer(user)
+
+ link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
+ doc = filter(link, current_user: user)
+
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'handles invalid Group references' do
+ link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
+
+ expect { filter(link) }.not_to raise_error
+ end
+ end
+
+ context 'with data-user' do
+ it 'allows any User reference' do
+ user = create(:user)
+
+ link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
+ doc = filter(link)
+
+ expect(doc.css('a').length).to eq 1
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb b/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb
new file mode 100644
index 00000000000..4fa473ad191
--- /dev/null
+++ b/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe ReferenceGathererFilter do
+ include ActionView::Helpers::UrlHelper
+ include FilterSpecHelper
+
+ def reference_link(data)
+ link_to('text', '', class: 'gfm', data: data)
+ end
+
+ context "for issue references" do
+
+ context 'with data-project' do
+ it 'removes unpermitted Project references' do
+ user = create(:user)
+ project = create(:empty_project)
+ issue = create(:issue, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name)
+ result = pipeline_result(link, current_user: user)
+
+ expect(result[:references][:issue]).to be_empty
+ end
+
+ it 'allows permitted Project references' do
+ user = create(:user)
+ project = create(:empty_project)
+ issue = create(:issue, project: project)
+ project.team << [user, :master]
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name)
+ result = pipeline_result(link, current_user: user)
+
+ expect(result[:references][:issue]).to eq([issue])
+ end
+
+ it 'handles invalid Project references' do
+ link = reference_link(project: 12345, issue: 12345, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name)
+
+ expect { pipeline_result(link) }.not_to raise_error
+ end
+ end
+ end
+
+ context "for user references" do
+
+ context 'with data-group' do
+ it 'removes unpermitted Group references' do
+ user = create(:user)
+ group = create(:group)
+
+ link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
+ result = pipeline_result(link, current_user: user)
+
+ expect(result[:references][:user]).to be_empty
+ end
+
+ it 'allows permitted Group references' do
+ user = create(:user)
+ group = create(:group)
+ group.add_developer(user)
+
+ link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
+ result = pipeline_result(link, current_user: user)
+
+ expect(result[:references][:user]).to eq([user])
+ end
+
+ it 'handles invalid Group references' do
+ link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
+
+ expect { pipeline_result(link) }.not_to raise_error
+ end
+ end
+
+ context 'with data-user' do
+ it 'allows any User reference' do
+ user = create(:user)
+
+ link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
+ result = pipeline_result(link)
+
+ expect(result[:references][:user]).to eq([user])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb
index e50c82d0b3c..27cd00e8054 100644
--- a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb
+++ b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb
@@ -44,7 +44,7 @@ module Gitlab::Markdown
instance = described_class.new('Foo')
3.times { instance.whitelist }
- expect(instance.whitelist[:transformers].size).to eq 4
+ expect(instance.whitelist[:transformers].size).to eq 5
end
it 'allows syntax highlighting' do
@@ -77,19 +77,100 @@ module Gitlab::Markdown
end
it 'removes `rel` attribute from `a` elements' do
- doc = filter(%q{<a href="#" rel="nofollow">Link</a>})
+ act = %q{<a href="#" rel="nofollow">Link</a>}
+ exp = %q{<a href="#">Link</a>}
- expect(doc.css('a').size).to eq 1
- expect(doc.at_css('a')['href']).to eq '#'
- expect(doc.at_css('a')['rel']).to be_nil
+ expect(filter(act).to_html).to eq exp
end
- it 'removes script-like `href` attribute from `a` elements' do
- html = %q{<a href="javascript:alert('Hi')">Hi</a>}
- doc = filter(html)
+ # Adapted from the Sanitize test suite: http://git.io/vczrM
+ protocols = {
+ 'protocol-based JS injection: simple, no spaces' => {
+ input: '<a href="javascript:alert(\'XSS\');">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: simple, spaces before' => {
+ input: '<a href="javascript :alert(\'XSS\');">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: simple, spaces after' => {
+ input: '<a href="javascript: alert(\'XSS\');">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: simple, spaces before and after' => {
+ input: '<a href="javascript : alert(\'XSS\');">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: preceding colon' => {
+ input: '<a href=":javascript:alert(\'XSS\');">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: UTF-8 encoding' => {
+ input: '<a href="javascript&#58;">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: long UTF-8 encoding' => {
+ input: '<a href="javascript&#0058;">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: long UTF-8 encoding without semicolons' => {
+ input: '<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: hex encoding' => {
+ input: '<a href="javascript&#x3A;">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: long hex encoding' => {
+ input: '<a href="javascript&#x003A;">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: hex encoding without semicolons' => {
+ input: '<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: null char' => {
+ input: "<a href=java\0script:alert(\"XSS\")>foo</a>",
+ output: '<a href="java"></a>'
+ },
+
+ 'protocol-based JS injection: spaces and entities' => {
+ input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
+ output: '<a href="">foo</a>'
+ },
+ }
+
+ protocols.each do |name, data|
+ it "handles #{name}" do
+ doc = filter(data[:input])
+
+ expect(doc.to_html).to eq data[:output]
+ end
+ end
+
+ it 'allows non-standard anchor schemes' do
+ exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>}
+ act = filter(exp)
+
+ expect(act.to_html).to eq exp
+ end
+
+ it 'allows relative links' do
+ exp = %q{<a href="foo/bar.md">foo/bar.md</a>}
+ act = filter(exp)
- expect(doc.css('a').size).to eq 1
- expect(doc.at_css('a')['href']).to be_nil
+ expect(act.to_html).to eq exp
end
end
diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb
index fd3f0d20fad..3a9acc9d6d4 100644
--- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb
+++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb
@@ -4,7 +4,7 @@ module Gitlab::Markdown
describe SnippetReferenceFilter do
include FilterSpecHelper
- let(:project) { create(:empty_project) }
+ let(:project) { create(:empty_project, :public) }
let(:snippet) { create(:project_snippet, project: project) }
let(:reference) { snippet.to_reference }
@@ -15,56 +15,64 @@ module Gitlab::Markdown
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Snippet #{reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
end
context 'internal reference' do
it 'links to a valid reference' do
- doc = filter("See #{reference}")
+ doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_snippet_url(project.namespace, project, snippet)
end
it 'links with adjacent text' do
- doc = filter("Snippet (#{reference}.)")
+ doc = reference_filter("Snippet (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid snippet IDs' do
exp = act = "Snippet #{invalidate_reference(reference)}"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
it 'includes a title attribute' do
- doc = filter("Snippet #{reference}")
+ doc = reference_filter("Snippet #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}"
end
it 'escapes the title attribute' do
snippet.update_attribute(:title, %{"></a>whatever<a title="})
- doc = filter("Snippet #{reference}")
+ doc = reference_filter("Snippet #{reference}")
expect(doc.text).to eq "Snippet #{reference}"
end
it 'includes default classes' do
- doc = filter("Snippet #{reference}")
+ doc = reference_filter("Snippet #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
end
- it 'includes a data-project-id attribute' do
- doc = filter("Snippet #{reference}")
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Snippet #{reference}")
link = doc.css('a').first
- expect(link).to have_attribute('data-project-id')
- expect(link.attr('data-project-id')).to eq project.id.to_s
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-snippet attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-snippet')
+ expect(link.attr('data-snippet')).to eq snippet.id.to_s
end
it 'supports an :only_path context' do
- doc = filter("Snippet #{reference}", only_path: true)
+ doc = reference_filter("Snippet #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
@@ -72,52 +80,68 @@ module Gitlab::Markdown
end
it 'adds to the results hash' do
- result = pipeline_result("Snippet #{reference}")
+ result = reference_pipeline_result("Snippet #{reference}")
expect(result[:references][:snippet]).to eq [snippet]
end
end
context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:empty_project, namespace: namespace) }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:snippet) { create(:project_snippet, project: project2) }
let(:reference) { snippet.to_reference(project) }
- context 'when user can access reference' do
- before { allow_cross_reference! }
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
+ end
- expect(doc.css('a').first.attr('href')).
- to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
- end
+ it 'links with adjacent text' do
+ doc = reference_filter("See (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
- it 'links with adjacent text' do
- doc = filter("See (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
- end
+ it 'ignores invalid snippet IDs on the referenced project' do
+ exp = act = "See #{invalidate_reference(reference)}"
- it 'ignores invalid snippet IDs on the referenced project' do
- exp = act = "See #{invalidate_reference(reference)}"
+ expect(reference_filter(act).to_html).to eq exp
+ end
- expect(filter(act).to_html).to eq exp
- end
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("Snippet #{reference}")
+ expect(result[:references][:snippet]).to eq [snippet]
+ end
+ end
+
+ context 'cross-project URL reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let(:snippet) { create(:project_snippet, project: project2) }
+ let(:reference) { urls.namespace_project_snippet_url(project2.namespace, project2, snippet) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
- it 'adds to the results hash' do
- result = pipeline_result("Snippet #{reference}")
- expect(result[:references][:snippet]).to eq [snippet]
- end
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
end
- context 'when user cannot access reference' do
- before { disallow_cross_reference! }
+ it 'links with adjacent text' do
+ doc = reference_filter("See (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid snippet IDs on the referenced project' do
+ act = "See #{invalidate_reference(reference)}"
- it 'ignores valid references' do
- exp = act = "See #{reference}"
+ expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
+ end
- expect(filter(act).to_html).to eq exp
- end
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("Snippet #{reference}")
+ expect(result[:references][:snippet]).to eq [snippet]
end
end
end
diff --git a/spec/lib/gitlab/markdown/upload_link_filter_spec.rb b/spec/lib/gitlab/markdown/upload_link_filter_spec.rb
new file mode 100644
index 00000000000..9ae45a6f559
--- /dev/null
+++ b/spec/lib/gitlab/markdown/upload_link_filter_spec.rb
@@ -0,0 +1,75 @@
+# encoding: UTF-8
+
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe UploadLinkFilter do
+ def filter(doc, contexts = {})
+ contexts.reverse_merge!({
+ project: project
+ })
+
+ described_class.call(doc, contexts)
+ end
+
+ def image(path)
+ %(<img src="#{path}" />)
+ end
+
+ def link(path)
+ %(<a href="#{path}">#{path}</a>)
+ end
+
+ let(:project) { create(:project) }
+
+ shared_examples :preserve_unchanged do
+ it 'does not modify any relative URL in anchor' do
+ doc = filter(link('README.md'))
+ expect(doc.at_css('a')['href']).to eq 'README.md'
+ end
+
+ it 'does not modify any relative URL in image' do
+ doc = filter(image('files/images/logo-black.png'))
+ expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png'
+ end
+ end
+
+ it 'does not raise an exception on invalid URIs' do
+ act = link("://foo")
+ expect { filter(act) }.not_to raise_error
+ end
+
+ context 'with a valid repository' do
+ it 'rebuilds relative URL for a link' do
+ doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.at_css('a')['href']).
+ to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+ end
+
+ it 'rebuilds relative URL for an image' do
+ doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.at_css('a')['href']).
+ to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+ end
+
+ it 'does not modify absolute URL' do
+ doc = filter(link('http://example.com'))
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ end
+
+ it 'supports Unicode filenames' do
+ path = '/uploads/한글.png'
+ escaped = Addressable::URI.escape(path)
+
+ # Stub these methods so the file doesn't actually need to be in the repo
+ allow_any_instance_of(described_class).
+ to receive(:file_exists?).and_return(true)
+ allow_any_instance_of(described_class).
+ to receive(:image?).with(path).and_return(true)
+
+ doc = filter(image(escaped))
+ expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png"
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
index b2155fab59b..25379f0670e 100644
--- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
+++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
@@ -4,7 +4,7 @@ module Gitlab::Markdown
describe UserReferenceFilter do
include FilterSpecHelper
- let(:project) { create(:empty_project) }
+ let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:reference) { user.to_reference }
@@ -14,13 +14,13 @@ module Gitlab::Markdown
it 'ignores invalid users' do
exp = act = "Hey #{invalidate_reference(reference)}"
- expect(filter(act).to_html).to eq(exp)
+ expect(reference_filter(act).to_html).to eq(exp)
end
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Hey #{reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
+ expect(reference_filter(act).to_html).to eq exp
end
end
@@ -32,110 +32,118 @@ module Gitlab::Markdown
end
it 'supports a special @all mention' do
- doc = filter("Hey #{reference}")
+ doc = reference_filter("Hey #{reference}")
expect(doc.css('a').length).to eq 1
expect(doc.css('a').first.attr('href'))
.to eq urls.namespace_project_url(project.namespace, project)
end
it 'adds to the results hash' do
- result = pipeline_result("Hey #{reference}")
+ result = reference_pipeline_result("Hey #{reference}")
expect(result[:references][:user]).to eq [project.creator]
end
end
context 'mentioning a user' do
it 'links to a User' do
- doc = filter("Hey #{reference}")
+ doc = reference_filter("Hey #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
end
it 'links to a User with a period' do
user = create(:user, name: 'alphA.Beta')
- doc = filter("Hey #{user.to_reference}")
+ doc = reference_filter("Hey #{user.to_reference}")
expect(doc.css('a').length).to eq 1
end
it 'links to a User with an underscore' do
user = create(:user, name: 'ping_pong_king')
- doc = filter("Hey #{user.to_reference}")
+ doc = reference_filter("Hey #{user.to_reference}")
expect(doc.css('a').length).to eq 1
end
- it 'includes a data-user-id attribute' do
- doc = filter("Hey #{reference}")
+ it 'includes a data-user attribute' do
+ doc = reference_filter("Hey #{reference}")
link = doc.css('a').first
- expect(link).to have_attribute('data-user-id')
- expect(link.attr('data-user-id')).to eq user.namespace.owner_id.to_s
+ expect(link).to have_attribute('data-user')
+ expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
end
it 'adds to the results hash' do
- result = pipeline_result("Hey #{reference}")
+ result = reference_pipeline_result("Hey #{reference}")
expect(result[:references][:user]).to eq [user]
end
end
context 'mentioning a group' do
let(:group) { create(:group) }
- let(:user) { create(:user) }
let(:reference) { group.to_reference }
- context 'that the current user can read' do
- before do
- group.add_developer(user)
- end
-
- it 'links to the Group' do
- doc = filter("Hey #{reference}", current_user: user)
- expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
- end
-
- it 'includes a data-group-id attribute' do
- doc = filter("Hey #{reference}", current_user: user)
- link = doc.css('a').first
+ it 'links to the Group' do
+ doc = reference_filter("Hey #{reference}")
+ expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
+ end
- expect(link).to have_attribute('data-group-id')
- expect(link.attr('data-group-id')).to eq group.id.to_s
- end
+ it 'includes a data-group attribute' do
+ doc = reference_filter("Hey #{reference}")
+ link = doc.css('a').first
- it 'adds to the results hash' do
- result = pipeline_result("Hey #{reference}", current_user: user)
- expect(result[:references][:user]).to eq group.users
- end
+ expect(link).to have_attribute('data-group')
+ expect(link.attr('data-group')).to eq group.id.to_s
end
- context 'that the current user cannot read' do
- it 'ignores references to the Group' do
- doc = filter("Hey #{reference}", current_user: user)
- expect(doc.to_html).to eq "Hey #{reference}"
- end
-
- it 'does not add to the results hash' do
- result = pipeline_result("Hey #{reference}", current_user: user)
- expect(result[:references][:user]).to eq []
- end
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("Hey #{reference}")
+ expect(result[:references][:user]).to eq group.users
end
end
it 'links with adjacent text' do
- doc = filter("Mention me (#{reference}.)")
+ doc = reference_filter("Mention me (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
end
it 'includes default classes' do
- doc = filter("Hey #{reference}")
+ doc = reference_filter("Hey #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
end
it 'supports an :only_path context' do
- doc = filter("Hey #{reference}", only_path: true)
+ doc = reference_filter("Hey #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).to eq urls.user_path(user)
end
+
+ context 'referencing a user in a link href' do
+ let(:reference) { %Q{<a href="#{user.to_reference}">User</a>} }
+
+ it 'links to a User' do
+ doc = reference_filter("Hey #{reference}")
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Mention me (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>User<\/a>\.\)/)
+ end
+
+ it 'includes a data-user attribute' do
+ doc = reference_filter("Hey #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-user')
+ expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
+ end
+
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("Hey #{reference}")
+ expect(result[:references][:user]).to eq [user]
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index c0083fc85be..fd3ab1fb7c8 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -19,10 +19,6 @@ describe Gitlab::OAuth::User do
let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
it "finds an existing user based on uid and provider (facebook)" do
- # FIXME (rspeicher): It's unlikely that this test is actually doing anything
- # `auth` is never used and removing it entirely doesn't break the test, so
- # what's it doing?
- auth = double(info: double(name: 'John'), uid: 'my-uid', provider: 'my-provider')
expect( oauth_user.persisted? ).to be_truthy
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 32a25f08cac..19327ac8ce0 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::ProjectSearchResults do
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to be_nil }
- it { expect(results.query).to eq('hello\\ world') }
+ it { expect(results.query).to eq('hello world') }
end
describe 'initialize with ref' do
@@ -18,6 +18,6 @@ describe Gitlab::ProjectSearchResults do
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
- it { expect(results.query).to eq('hello\\ world') }
+ it { expect(results.query).to eq('hello world') }
end
end
diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb
index 1b8ba7b4d43..2170399ab5c 100644
--- a/spec/lib/gitlab/push_data_builder_spec.rb
+++ b/spec/lib/gitlab/push_data_builder_spec.rb
@@ -17,6 +17,9 @@ describe 'Gitlab::PushDataBuilder' do
it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) }
it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) }
it { expect(data[:total_commits_count]).to eq(3) }
+ it { expect(data[:commits].first[:added]).to eq(["gitlab-grack"]) }
+ it { expect(data[:commits].first[:modified]).to eq([".gitmodules"]) }
+ it { expect(data[:commits].first[:removed]).to eq([]) }
end
describe :build do
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 088e34f050c..ad84d2274e8 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::ReferenceExtractor do
project.team << [@u_bar, :guest]
subject.analyze('@foo, @baduser, @bar, and @offteam')
- expect(subject.users).to eq([@u_foo, @u_bar, @u_offteam])
+ expect(subject.users).to match_array([@u_foo, @u_bar, @u_offteam])
end
it 'ignores user mentions inside specific elements' do
@@ -37,7 +37,7 @@ describe Gitlab::ReferenceExtractor do
> @offteam
})
- expect(subject.users).to eq([])
+ expect(subject.users).to match_array([])
end
it 'accesses valid issue objects' do
@@ -45,7 +45,7 @@ describe Gitlab::ReferenceExtractor do
@i1 = create(:issue, project: project)
subject.analyze("#{@i0.to_reference}, #{@i1.to_reference}, and #{Issue.reference_prefix}999.")
- expect(subject.issues).to eq([@i0, @i1])
+ expect(subject.issues).to match_array([@i0, @i1])
end
it 'accesses valid merge requests' do
@@ -53,7 +53,7 @@ describe Gitlab::ReferenceExtractor do
@m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'feature_conflict')
subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.")
- expect(subject.merge_requests).to eq([@m1, @m0])
+ expect(subject.merge_requests).to match_array([@m1, @m0])
end
it 'accesses valid labels' do
@@ -62,7 +62,7 @@ describe Gitlab::ReferenceExtractor do
@l2 = create(:label)
subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}")
- expect(subject.labels).to eq([@l0, @l1])
+ expect(subject.labels).to match_array([@l0, @l1])
end
it 'accesses valid snippets' do
@@ -71,7 +71,7 @@ describe Gitlab::ReferenceExtractor do
@s2 = create(:project_snippet)
subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}")
- expect(subject.snippets).to eq([@s0, @s1])
+ expect(subject.snippets).to match_array([@s0, @s1])
end
it 'accesses valid commits' do
@@ -109,7 +109,7 @@ describe Gitlab::ReferenceExtractor do
subject.analyze("this refers issue #{issue.to_reference(project)}")
extracted = subject.issues
expect(extracted.size).to eq(1)
- expect(extracted).to eq([issue])
+ expect(extracted).to match_array([issue])
end
end
end
diff --git a/spec/lib/gitlab/sherlock/collection_spec.rb b/spec/lib/gitlab/sherlock/collection_spec.rb
new file mode 100644
index 00000000000..a8a9d6fc7bc
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/collection_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::Collection do
+ let(:collection) { described_class.new }
+
+ let(:transaction) do
+ Gitlab::Sherlock::Transaction.new('POST', '/cat_pictures')
+ end
+
+ describe '#add' do
+ it 'adds a new transaction' do
+ collection.add(transaction)
+
+ expect(collection).to_not be_empty
+ end
+
+ it 'is aliased as <<' do
+ collection << transaction
+
+ expect(collection).to_not be_empty
+ end
+ end
+
+ describe '#each' do
+ it 'iterates over every transaction' do
+ collection.add(transaction)
+
+ expect { |b| collection.each(&b) }.to yield_with_args(transaction)
+ end
+ end
+
+ describe '#clear' do
+ it 'removes all transactions' do
+ collection.add(transaction)
+
+ collection.clear
+
+ expect(collection).to be_empty
+ end
+ end
+
+ describe '#empty?' do
+ it 'returns true for an empty collection' do
+ expect(collection).to be_empty
+ end
+
+ it 'returns false for a collection with a transaction' do
+ collection.add(transaction)
+
+ expect(collection).to_not be_empty
+ end
+ end
+
+ describe '#find_transaction' do
+ it 'returns the transaction for the given ID' do
+ collection.add(transaction)
+
+ expect(collection.find_transaction(transaction.id)).to eq(transaction)
+ end
+
+ it 'returns nil when no transaction could be found' do
+ collection.add(transaction)
+
+ expect(collection.find_transaction('cats')).to be_nil
+ end
+ end
+
+ describe '#newest_first' do
+ it 'returns transactions sorted from new to old' do
+ trans1 = Gitlab::Sherlock::Transaction.new('POST', '/cat_pictures')
+ trans2 = Gitlab::Sherlock::Transaction.new('POST', '/more_cat_pictures')
+
+ allow(trans1).to receive(:finished_at).and_return(Time.utc(2015, 1, 1))
+ allow(trans2).to receive(:finished_at).and_return(Time.utc(2015, 1, 2))
+
+ collection.add(trans1)
+ collection.add(trans2)
+
+ expect(collection.newest_first).to eq([trans2, trans1])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/file_sample_spec.rb b/spec/lib/gitlab/sherlock/file_sample_spec.rb
new file mode 100644
index 00000000000..f05a59f56f6
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/file_sample_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::FileSample do
+ let(:sample) { described_class.new(__FILE__, [], 150.4, 2) }
+
+ describe '#id' do
+ it 'returns the ID' do
+ expect(sample.id).to be_an_instance_of(String)
+ end
+ end
+
+ describe '#file' do
+ it 'returns the file path' do
+ expect(sample.file).to eq(__FILE__)
+ end
+ end
+
+ describe '#line_samples' do
+ it 'returns the line samples' do
+ expect(sample.line_samples).to eq([])
+ end
+ end
+
+ describe '#events' do
+ it 'returns the total number of events' do
+ expect(sample.events).to eq(2)
+ end
+ end
+
+ describe '#duration' do
+ it 'returns the total execution time' do
+ expect(sample.duration).to eq(150.4)
+ end
+ end
+
+ describe '#relative_path' do
+ it 'returns the relative path' do
+ expect(sample.relative_path).
+ to eq('spec/lib/gitlab/sherlock/file_sample_spec.rb')
+ end
+ end
+
+ describe '#to_param' do
+ it 'returns the sample ID' do
+ expect(sample.to_param).to eq(sample.id)
+ end
+ end
+
+ describe '#source' do
+ it 'returns the contents of the file' do
+ expect(sample.source).to eq(File.read(__FILE__))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/line_profiler_spec.rb b/spec/lib/gitlab/sherlock/line_profiler_spec.rb
new file mode 100644
index 00000000000..8f2e1299714
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/line_profiler_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::LineProfiler do
+ let(:profiler) { described_class.new }
+
+ describe '#profile' do
+ it 'runs the profiler when using MRI' do
+ allow(profiler).to receive(:mri?).and_return(true)
+ allow(profiler).to receive(:profile_mri)
+
+ profiler.profile { 'cats' }
+ end
+
+ it 'raises NotImplementedError when profiling an unsupported platform' do
+ allow(profiler).to receive(:mri?).and_return(false)
+
+ expect { profiler.profile { 'cats' } }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#profile_mri' do
+ it 'returns an Array containing the return value and profiling samples' do
+ allow(profiler).to receive(:lineprof).
+ and_yield.
+ and_return({ __FILE__ => [[0, 0, 0, 0]] })
+
+ retval, samples = profiler.profile_mri { 42 }
+
+ expect(retval).to eq(42)
+ expect(samples).to eq([])
+ end
+ end
+
+ describe '#aggregate_rblineprof' do
+ let(:raw_samples) do
+ { __FILE__ => [[30000, 30000, 5, 0], [15000, 15000, 4, 0]] }
+ end
+
+ it 'returns an Array of FileSample objects' do
+ samples = profiler.aggregate_rblineprof(raw_samples)
+
+ expect(samples).to be_an_instance_of(Array)
+ expect(samples[0]).to be_an_instance_of(Gitlab::Sherlock::FileSample)
+ end
+
+ describe 'the first FileSample object' do
+ let(:file_sample) do
+ profiler.aggregate_rblineprof(raw_samples)[0]
+ end
+
+ it 'uses the correct file path' do
+ expect(file_sample.file).to eq(__FILE__)
+ end
+
+ it 'contains a list of line samples' do
+ line_sample = file_sample.line_samples[0]
+
+ expect(line_sample).to be_an_instance_of(Gitlab::Sherlock::LineSample)
+
+ expect(line_sample.duration).to eq(15.0)
+ expect(line_sample.events).to eq(4)
+ end
+
+ it 'contains the total file execution time' do
+ expect(file_sample.duration).to eq(30.0)
+ end
+
+ it 'contains the total amount of file events' do
+ expect(file_sample.events).to eq(5)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/line_sample_spec.rb b/spec/lib/gitlab/sherlock/line_sample_spec.rb
new file mode 100644
index 00000000000..5f02f6a3213
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/line_sample_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::LineSample do
+ let(:sample) { described_class.new(150.0, 4) }
+
+ describe '#duration' do
+ it 'returns the duration' do
+ expect(sample.duration).to eq(150.0)
+ end
+ end
+
+ describe '#events' do
+ it 'returns the amount of events' do
+ expect(sample.events).to eq(4)
+ end
+ end
+
+ describe '#percentage_of' do
+ it 'returns the percentage of 1500.0' do
+ expect(sample.percentage_of(1500.0)).to be_within(0.1).of(10.0)
+ end
+ end
+
+ describe '#majority_of' do
+ it 'returns true if the sample takes up the majority of the given duration' do
+ expect(sample.majority_of?(500.0)).to eq(true)
+ end
+
+ it "returns false if the sample doesn't take up the majority of the given duration" do
+ expect(sample.majority_of?(1500.0)).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/location_spec.rb b/spec/lib/gitlab/sherlock/location_spec.rb
new file mode 100644
index 00000000000..b295a624b35
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/location_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::Location do
+ let(:location) { described_class.new(__FILE__, 1) }
+
+ describe 'from_ruby_location' do
+ it 'creates a Location from a Thread::Backtrace::Location' do
+ input = caller_locations[0]
+ output = described_class.from_ruby_location(input)
+
+ expect(output).to be_an_instance_of(described_class)
+ expect(output.path).to eq(input.path)
+ expect(output.line).to eq(input.lineno)
+ end
+ end
+
+ describe '#path' do
+ it 'returns the file path' do
+ expect(location.path).to eq(__FILE__)
+ end
+ end
+
+ describe '#line' do
+ it 'returns the line number' do
+ expect(location.line).to eq(1)
+ end
+ end
+
+ describe '#application?' do
+ it 'returns true for an application frame' do
+ expect(location.application?).to eq(true)
+ end
+
+ it 'returns false for a non application frame' do
+ loc = described_class.new('/tmp/cats.rb', 1)
+
+ expect(loc.application?).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/middleware_spec.rb b/spec/lib/gitlab/sherlock/middleware_spec.rb
new file mode 100644
index 00000000000..aa74fc53a79
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/middleware_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::Middleware do
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+
+ describe '#call' do
+ describe 'when instrumentation is enabled' do
+ it 'instruments a request' do
+ allow(middleware).to receive(:instrument?).and_return(true)
+ allow(middleware).to receive(:call_with_instrumentation)
+
+ middleware.call({})
+ end
+ end
+
+ describe 'when instrumentation is disabled' do
+ it "doesn't instrument a request" do
+ allow(middleware).to receive(:instrument).and_return(false)
+ allow(app).to receive(:call)
+
+ middleware.call({})
+ end
+ end
+ end
+
+ describe '#call_with_instrumentation' do
+ it 'instruments a request' do
+ trans = double(:transaction)
+ retval = 'cats are amazing'
+ env = {}
+
+ allow(app).to receive(:call).with(env).and_return(retval)
+ allow(middleware).to receive(:transaction_from_env).and_return(trans)
+ allow(trans).to receive(:run).and_yield.and_return(retval)
+ allow(Gitlab::Sherlock.collection).to receive(:add).with(trans)
+
+ middleware.call_with_instrumentation(env)
+ end
+ end
+
+ describe '#instrument?' do
+ it 'returns false for a text/css request' do
+ env = { 'HTTP_ACCEPT' => 'text/css', 'REQUEST_URI' => '/' }
+
+ expect(middleware.instrument?(env)).to eq(false)
+ end
+
+ it 'returns false for a request to a Sherlock route' do
+ env = {
+ 'HTTP_ACCEPT' => 'text/html',
+ 'REQUEST_URI' => '/sherlock/transactions'
+ }
+
+ expect(middleware.instrument?(env)).to eq(false)
+ end
+
+ it 'returns true for a request that should be instrumented' do
+ env = {
+ 'HTTP_ACCEPT' => 'text/html',
+ 'REQUEST_URI' => '/cats'
+ }
+
+ expect(middleware.instrument?(env)).to eq(true)
+ end
+ end
+
+ describe '#transaction_from_env' do
+ it 'returns a Transaction' do
+ env = {
+ 'HTTP_ACCEPT' => 'text/html',
+ 'REQUEST_URI' => '/cats'
+ }
+
+ expect(middleware.transaction_from_env(env)).
+ to be_an_instance_of(Gitlab::Sherlock::Transaction)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/query_spec.rb b/spec/lib/gitlab/sherlock/query_spec.rb
new file mode 100644
index 00000000000..a9afef5dc1d
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/query_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::Query do
+ let(:started_at) { Time.utc(2015, 1, 1) }
+ let(:finished_at) { started_at + 5 }
+
+ let(:query) do
+ described_class.new('SELECT COUNT(*) FROM users', started_at, finished_at)
+ end
+
+ describe 'new_with_bindings' do
+ it 'returns a Query' do
+ sql = 'SELECT COUNT(*) FROM users WHERE id = $1'
+ bindings = [[double(:column), 10]]
+
+ query = described_class.
+ new_with_bindings(sql, bindings, started_at, finished_at)
+
+ expect(query.query).to eq('SELECT COUNT(*) FROM users WHERE id = 10;')
+ end
+ end
+
+ describe '#id' do
+ it 'returns a String' do
+ expect(query.id).to be_an_instance_of(String)
+ end
+ end
+
+ describe '#query' do
+ it 'returns the query with a trailing semi-colon' do
+ expect(query.query).to eq('SELECT COUNT(*) FROM users;')
+ end
+ end
+
+ describe '#started_at' do
+ it 'returns the start time' do
+ expect(query.started_at).to eq(started_at)
+ end
+ end
+
+ describe '#finished_at' do
+ it 'returns the completion time' do
+ expect(query.finished_at).to eq(finished_at)
+ end
+ end
+
+ describe '#backtrace' do
+ it 'returns the backtrace' do
+ expect(query.backtrace).to be_an_instance_of(Array)
+ end
+ end
+
+ describe '#duration' do
+ it 'returns the duration in milliseconds' do
+ expect(query.duration).to be_within(0.1).of(5000.0)
+ end
+ end
+
+ describe '#to_param' do
+ it 'returns the query ID' do
+ expect(query.to_param).to eq(query.id)
+ end
+ end
+
+ describe '#formatted_query' do
+ it 'returns a formatted version of the query' do
+ expect(query.formatted_query).to eq(<<-EOF.strip)
+SELECT COUNT(*)
+FROM users;
+ EOF
+ end
+ end
+
+ describe '#last_application_frame' do
+ it 'returns the last application frame' do
+ frame = query.last_application_frame
+
+ expect(frame).to be_an_instance_of(Gitlab::Sherlock::Location)
+ expect(frame.path).to eq(__FILE__)
+ end
+ end
+
+ describe '#application_backtrace' do
+ it 'returns an Array of application frames' do
+ frames = query.application_backtrace
+
+ expect(frames).to be_an_instance_of(Array)
+ expect(frames).to_not be_empty
+
+ frames.each do |frame|
+ expect(frame.path).to start_with(Rails.root.to_s)
+ end
+ end
+ end
+
+ describe '#explain' do
+ it 'returns the query plan as a String' do
+ lines = [
+ ['Aggregate (cost=123 rows=1)'],
+ [' -> Index Only Scan using index_cats_are_amazing']
+ ]
+
+ result = double(:result, values: lines)
+
+ allow(query).to receive(:raw_explain).and_return(result)
+
+ expect(query.explain).to eq(<<-EOF.strip)
+Aggregate (cost=123 rows=1)
+ -> Index Only Scan using index_cats_are_amazing
+ EOF
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/transaction_spec.rb b/spec/lib/gitlab/sherlock/transaction_spec.rb
new file mode 100644
index 00000000000..fb80c62c794
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/transaction_spec.rb
@@ -0,0 +1,235 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::Transaction do
+ let(:transaction) { described_class.new('POST', '/cat_pictures') }
+
+ describe '#id' do
+ it 'returns the transaction ID' do
+ expect(transaction.id).to be_an_instance_of(String)
+ end
+ end
+
+ describe '#type' do
+ it 'returns the type' do
+ expect(transaction.type).to eq('POST')
+ end
+ end
+
+ describe '#path' do
+ it 'returns the path' do
+ expect(transaction.path).to eq('/cat_pictures')
+ end
+ end
+
+ describe '#queries' do
+ it 'returns an Array of queries' do
+ expect(transaction.queries).to be_an_instance_of(Array)
+ end
+ end
+
+ describe '#file_samples' do
+ it 'returns an Array of file samples' do
+ expect(transaction.file_samples).to be_an_instance_of(Array)
+ end
+ end
+
+ describe '#started_at' do
+ it 'returns the start time' do
+ allow(transaction).to receive(:profile_lines).and_yield
+
+ transaction.run { 'cats are amazing' }
+
+ expect(transaction.started_at).to be_an_instance_of(Time)
+ end
+ end
+
+ describe '#finished_at' do
+ it 'returns the completion time' do
+ allow(transaction).to receive(:profile_lines).and_yield
+
+ transaction.run { 'cats are amazing' }
+
+ expect(transaction.finished_at).to be_an_instance_of(Time)
+ end
+ end
+
+ describe '#view_counts' do
+ it 'returns a Hash' do
+ expect(transaction.view_counts).to be_an_instance_of(Hash)
+ end
+
+ it 'sets the default value of a key to 0' do
+ expect(transaction.view_counts['cats.rb']).to be_zero
+ end
+ end
+
+ describe '#run' do
+ it 'runs the transaction' do
+ allow(transaction).to receive(:profile_lines).and_yield
+
+ retval = transaction.run { 'cats are amazing' }
+
+ expect(retval).to eq('cats are amazing')
+ end
+ end
+
+ describe '#duration' do
+ it 'returns the duration in seconds' do
+ start_time = Time.now
+
+ allow(transaction).to receive(:started_at).and_return(start_time)
+ allow(transaction).to receive(:finished_at).and_return(start_time + 5)
+
+ expect(transaction.duration).to be_within(0.1).of(5.0)
+ end
+ end
+
+ describe '#query_duration' do
+ it 'returns the total query duration in seconds' do
+ time = Time.now
+ query1 = Gitlab::Sherlock::Query.new('SELECT 1', time, time + 5)
+ query2 = Gitlab::Sherlock::Query.new('SELECT 2', time, time + 2)
+
+ transaction.queries << query1
+ transaction.queries << query2
+
+ expect(transaction.query_duration).to be_within(0.1).of(7.0)
+ end
+ end
+
+ describe '#to_param' do
+ it 'returns the transaction ID' do
+ expect(transaction.to_param).to eq(transaction.id)
+ end
+ end
+
+ describe '#sorted_queries' do
+ it 'returns the queries in descending order' do
+ start_time = Time.now
+
+ query1 = Gitlab::Sherlock::Query.new('SELECT 1', start_time, start_time)
+
+ query2 = Gitlab::Sherlock::Query.
+ new('SELECT 2', start_time, start_time + 5)
+
+ transaction.queries << query1
+ transaction.queries << query2
+
+ expect(transaction.sorted_queries).to eq([query2, query1])
+ end
+ end
+
+ describe '#sorted_file_samples' do
+ it 'returns the file samples in descending order' do
+ sample1 = Gitlab::Sherlock::FileSample.new(__FILE__, [], 10.0, 1)
+ sample2 = Gitlab::Sherlock::FileSample.new(__FILE__, [], 15.0, 1)
+
+ transaction.file_samples << sample1
+ transaction.file_samples << sample2
+
+ expect(transaction.sorted_file_samples).to eq([sample2, sample1])
+ end
+ end
+
+ describe '#find_query' do
+ it 'returns a Query when found' do
+ query = Gitlab::Sherlock::Query.new('SELECT 1', Time.now, Time.now)
+
+ transaction.queries << query
+
+ expect(transaction.find_query(query.id)).to eq(query)
+ end
+
+ it 'returns nil when no query could be found' do
+ expect(transaction.find_query('cats')).to be_nil
+ end
+ end
+
+ describe '#find_file_sample' do
+ it 'returns a FileSample when found' do
+ sample = Gitlab::Sherlock::FileSample.new(__FILE__, [], 10.0, 1)
+
+ transaction.file_samples << sample
+
+ expect(transaction.find_file_sample(sample.id)).to eq(sample)
+ end
+
+ it 'returns nil when no file sample could be found' do
+ expect(transaction.find_file_sample('cats')).to be_nil
+ end
+ end
+
+ describe '#profile_lines' do
+ describe 'when line profiling is enabled' do
+ it 'yields the block using the line profiler' do
+ allow(Gitlab::Sherlock).to receive(:enable_line_profiler?).
+ and_return(true)
+
+ allow_any_instance_of(Gitlab::Sherlock::LineProfiler).
+ to receive(:profile).and_return('cats are amazing', [])
+
+ retval = transaction.profile_lines { 'cats are amazing' }
+
+ expect(retval).to eq('cats are amazing')
+ end
+ end
+
+ describe 'when line profiling is disabled' do
+ it 'yields the block' do
+ allow(Gitlab::Sherlock).to receive(:enable_line_profiler?).
+ and_return(false)
+
+ retval = transaction.profile_lines { 'cats are amazing' }
+
+ expect(retval).to eq('cats are amazing')
+ end
+ end
+ end
+
+ describe '#subscribe_to_active_record' do
+ let(:subscription) { transaction.subscribe_to_active_record }
+ let(:time) { Time.now }
+ let(:query_data) { { sql: 'SELECT 1', binds: [] } }
+
+ after do
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+
+ it 'tracks executed queries' do
+ expect(transaction).to receive(:track_query).
+ with('SELECT 1', [], time, time)
+
+ subscription.publish('test', time, time, nil, query_data)
+ end
+
+ it 'only tracks queries triggered from the transaction thread' do
+ expect(transaction).to_not receive(:track_query)
+
+ Thread.new { subscription.publish('test', time, time, nil, query_data) }.
+ join
+ end
+ end
+
+ describe '#subscribe_to_action_view' do
+ let(:subscription) { transaction.subscribe_to_action_view }
+ let(:time) { Time.now }
+ let(:view_data) { { identifier: 'foo.rb' } }
+
+ after do
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+
+ it 'tracks rendered views' do
+ expect(transaction).to receive(:track_view).with('foo.rb')
+
+ subscription.publish('test', time, time, nil, view_data)
+ end
+
+ it 'only tracks views rendered from the transaction thread' do
+ expect(transaction).to_not receive(:track_view)
+
+ Thread.new { subscription.publish('test', time, time, nil, view_data) }.
+ join
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb
new file mode 100644
index 00000000000..9e1cd4419e0
--- /dev/null
+++ b/spec/lib/gitlab/sql/union_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Gitlab::SQL::Union do
+ describe '#to_sql' do
+ it 'returns a String joining relations together using a UNION' do
+ rel1 = User.where(email: 'alice@example.com')
+ rel2 = User.where(email: 'bob@example.com')
+ union = described_class.new([rel1, rel2])
+
+ sql1 = rel1.reorder(nil).to_sql
+ sql2 = rel2.reorder(nil).to_sql
+
+ expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/uploads_transfer_spec.rb
new file mode 100644
index 00000000000..260364a513e
--- /dev/null
+++ b/spec/lib/gitlab/uploads_transfer_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Gitlab::UploadsTransfer do
+ before do
+ @root_dir = File.join(Rails.root, "public", "uploads")
+ @upload_transfer = Gitlab::UploadsTransfer.new
+
+ @project_path_was = "test_project_was"
+ @project_path = "test_project"
+ @namespace_path_was = "test_namespace_was"
+ @namespace_path = "test_namespace"
+ end
+
+ after do
+ FileUtils.rm_rf([
+ File.join(@root_dir, @namespace_path),
+ File.join(@root_dir, @namespace_path_was)
+ ])
+ end
+
+ describe '#move_project' do
+ it "moves project upload to another namespace" do
+ FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
+ @upload_transfer.move_project(@project_path, @namespace_path_was, @namespace_path)
+
+ expected_path = File.join(@root_dir, @namespace_path, @project_path)
+ expect(Dir.exist?(expected_path)).to be_truthy
+ end
+ end
+
+ describe '#rename_project' do
+ it "renames project" do
+ FileUtils.mkdir_p(File.join(@root_dir, @namespace_path, @project_path_was))
+ @upload_transfer.rename_project(@project_path_was, @project_path, @namespace_path)
+
+ expected_path = File.join(@root_dir, @namespace_path, @project_path)
+ expect(Dir.exist?(expected_path)).to be_truthy
+ end
+ end
+
+ describe '#rename_namespace' do
+ it "renames namespace" do
+ FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
+ @upload_transfer.rename_namespace(@namespace_path_was, @namespace_path)
+
+ expected_path = File.join(@root_dir, @namespace_path, @project_path)
+ expect(Dir.exist?(expected_path)).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/votes_spec.rb b/spec/lib/votes_spec.rb
deleted file mode 100644
index 39e5d054e62..00000000000
--- a/spec/lib/votes_spec.rb
+++ /dev/null
@@ -1,188 +0,0 @@
-require 'spec_helper'
-
-describe Issue, 'Votes' do
- let(:issue) { create(:issue) }
-
- describe "#upvotes" do
- it "with no notes has a 0/0 score" do
- expect(issue.upvotes).to eq(0)
- end
-
- it "should recognize non-+1 notes" do
- add_note "No +1 here"
- expect(issue.notes.size).to eq(1)
- expect(issue.notes.first.upvote?).to be_falsey
- expect(issue.upvotes).to eq(0)
- end
-
- it "should recognize a single +1 note" do
- add_note "+1 This is awesome"
- expect(issue.upvotes).to eq(1)
- end
-
- it 'should recognize multiple +1 notes' do
- add_note '+1 This is awesome', create(:user)
- add_note '+1 I want this', create(:user)
- expect(issue.upvotes).to eq(2)
- end
-
- it 'should not count 2 +1 votes from the same user' do
- add_note '+1 This is awesome'
- add_note '+1 I want this'
- expect(issue.upvotes).to eq(1)
- end
- end
-
- describe "#downvotes" do
- it "with no notes has a 0/0 score" do
- expect(issue.downvotes).to eq(0)
- end
-
- it "should recognize non--1 notes" do
- add_note "Almost got a -1"
- expect(issue.notes.size).to eq(1)
- expect(issue.notes.first.downvote?).to be_falsey
- expect(issue.downvotes).to eq(0)
- end
-
- it "should recognize a single -1 note" do
- add_note "-1 This is bad"
- expect(issue.downvotes).to eq(1)
- end
-
- it "should recognize multiple -1 notes" do
- add_note('-1 This is bad', create(:user))
- add_note('-1 Away with this', create(:user))
- expect(issue.downvotes).to eq(2)
- end
- end
-
- describe "#votes_count" do
- it "with no notes has a 0/0 score" do
- expect(issue.votes_count).to eq(0)
- end
-
- it "should recognize non notes" do
- add_note "No +1 here"
- expect(issue.notes.size).to eq(1)
- expect(issue.votes_count).to eq(0)
- end
-
- it "should recognize a single +1 note" do
- add_note "+1 This is awesome"
- expect(issue.votes_count).to eq(1)
- end
-
- it "should recognize a single -1 note" do
- add_note "-1 This is bad"
- expect(issue.votes_count).to eq(1)
- end
-
- it "should recognize multiple notes" do
- add_note('+1 This is awesome', create(:user))
- add_note('-1 This is bad', create(:user))
- add_note('+1 I want this', create(:user))
- expect(issue.votes_count).to eq(3)
- end
-
- it 'should not count 2 -1 votes from the same user' do
- add_note '-1 This is suspicious'
- add_note '-1 This is bad'
- expect(issue.votes_count).to eq(1)
- end
- end
-
- describe "#upvotes_in_percent" do
- it "with no notes has a 0% score" do
- expect(issue.upvotes_in_percent).to eq(0)
- end
-
- it "should count a single 1 note as 100%" do
- add_note "+1 This is awesome"
- expect(issue.upvotes_in_percent).to eq(100)
- end
-
- it 'should count multiple +1 notes as 100%' do
- add_note('+1 This is awesome', create(:user))
- add_note('+1 I want this', create(:user))
- expect(issue.upvotes_in_percent).to eq(100)
- end
-
- it 'should count fractions for multiple +1 and -1 notes correctly' do
- add_note('+1 This is awesome', create(:user))
- add_note('+1 I want this', create(:user))
- add_note('-1 This is bad', create(:user))
- add_note('+1 me too', create(:user))
- expect(issue.upvotes_in_percent).to eq(75)
- end
- end
-
- describe "#downvotes_in_percent" do
- it "with no notes has a 0% score" do
- expect(issue.downvotes_in_percent).to eq(0)
- end
-
- it "should count a single -1 note as 100%" do
- add_note "-1 This is bad"
- expect(issue.downvotes_in_percent).to eq(100)
- end
-
- it 'should count multiple -1 notes as 100%' do
- add_note('-1 This is bad', create(:user))
- add_note('-1 Away with this', create(:user))
- expect(issue.downvotes_in_percent).to eq(100)
- end
-
- it 'should count fractions for multiple +1 and -1 notes correctly' do
- add_note('+1 This is awesome', create(:user))
- add_note('+1 I want this', create(:user))
- add_note('-1 This is bad', create(:user))
- add_note('+1 me too', create(:user))
- expect(issue.downvotes_in_percent).to eq(25)
- end
- end
-
- describe '#filter_superceded_votes' do
-
- it 'should count a users vote only once amongst multiple votes' do
- add_note('-1 This needs work before I will accept it')
- add_note('+1 I want this', create(:user))
- add_note('+1 This is is awesome', create(:user))
- add_note('+1 this looks good now')
- add_note('+1 This is awesome', create(:user))
- add_note('+1 me too', create(:user))
- expect(issue.downvotes).to eq(0)
- expect(issue.upvotes).to eq(5)
- end
-
- it 'should count each users vote only once' do
- add_note '-1 This needs work before it will be accepted'
- add_note '+1 I like this'
- add_note '+1 I still like this'
- add_note '+1 I really like this'
- add_note '+1 Give me this now!!!!'
- expect(issue.downvotes).to eq(0)
- expect(issue.upvotes).to eq(1)
- end
-
- it 'should count a users vote only once without caring about comments' do
- add_note '-1 This needs work before it will be accepted'
- add_note 'Comment 1'
- add_note 'Another comment'
- add_note '+1 vote'
- add_note 'final comment'
- expect(issue.downvotes).to eq(0)
- expect(issue.upvotes).to eq(1)
- end
-
- end
-
- def add_note(text, author = issue.author)
- created_at = Time.now - 1.hour + Note.count.seconds
- issue.notes << create(:note,
- note: text,
- project: issue.project,
- author_id: author.id,
- created_at: created_at)
- end
-end
diff --git a/spec/mailers/ci/notify_spec.rb b/spec/mailers/ci/notify_spec.rb
index 20d8ddcd135..b83fb41603b 100644
--- a/spec/mailers/ci/notify_spec.rb
+++ b/spec/mailers/ci/notify_spec.rb
@@ -5,8 +5,7 @@ describe Ci::Notify do
include EmailSpec::Matchers
before do
- @project = FactoryGirl.create :ci_project
- @commit = FactoryGirl.create :ci_commit, project: @project
+ @commit = FactoryGirl.create :ci_commit
@build = FactoryGirl.create :ci_build, commit: @commit
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 2c97a521d96..27e509933b2 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -52,6 +52,7 @@ describe Notify do
end
it 'has headers that reference an existing thread' do
+ is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/
is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
is_expected.to have_header 'X-GitLab-Project', /#{project.name}/
@@ -76,6 +77,32 @@ describe Notify do
end
end
+ shared_examples 'it should have Gmail Actions links' do
+ it { is_expected.to have_body_text /ViewAction/ }
+ end
+
+ shared_examples 'it should not have Gmail Actions links' do
+ it { is_expected.to_not have_body_text /ViewAction/ }
+ end
+
+ shared_examples 'it should show Gmail Actions View Issue link' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it { is_expected.to have_body_text /View Issue/ }
+ end
+
+ shared_examples 'it should show Gmail Actions View Merge request link' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it { is_expected.to have_body_text /View Merge request/ }
+ end
+
+ shared_examples 'it should show Gmail Actions View Commit link' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it { is_expected.to have_body_text /View Commit/ }
+ end
+
describe 'for new users, the email' do
let(:example_site_path) { root_path }
let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) }
@@ -86,6 +113,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'a new user email', new_user_address
+ it_behaves_like 'it should not have Gmail Actions links'
it 'contains the password text' do
is_expected.to have_body_text /Click here to set your password/
@@ -114,6 +142,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'a new user email', new_user_address
+ it_behaves_like 'it should not have Gmail Actions links'
it 'should not contain the new user\'s password' do
is_expected.not_to have_body_text /password/
@@ -126,6 +155,7 @@ describe Notify do
subject { Notify.new_ssh_key_email(key.id) }
it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
it 'is sent to the new user' do
is_expected.to deliver_to key.user.email
@@ -149,6 +179,8 @@ describe Notify do
subject { Notify.new_email_email(email.id) }
+ it_behaves_like 'it should not have Gmail Actions links'
+
it 'is sent to the new user' do
is_expected.to deliver_to email.user.email
end
@@ -193,6 +225,7 @@ describe Notify do
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'issue'
+ it_behaves_like 'it should show Gmail Actions View Issue link'
it 'has the correct subject' do
is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/
@@ -206,16 +239,19 @@ describe Notify do
describe 'that are new with a description' do
subject { Notify.new_issue_email(issue_with_description.assignee_id, issue_with_description.id) }
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+
it 'contains the description' do
is_expected.to have_body_text /#{issue_with_description.description}/
end
end
describe 'that have been reassigned' do
- subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user) }
+ subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'it should show Gmail Actions View Issue link'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -242,9 +278,10 @@ describe Notify do
describe 'status changed' do
let(:status) { 'closed' }
- subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user) }
+ subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'it should show Gmail Actions View Issue link'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -268,7 +305,6 @@ describe Notify do
is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
end
end
-
end
context 'for merge requests' do
@@ -281,6 +317,7 @@ describe Notify do
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'merge_request'
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
@@ -306,6 +343,8 @@ describe Notify do
describe 'that are new with a description' do
subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) }
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+
it 'contains the description' do
is_expected.to have_body_text /#{merge_request_with_description.description}/
end
@@ -316,6 +355,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -342,9 +382,10 @@ describe Notify do
describe 'status changed' do
let(:status) { 'reopened' }
- subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user) }
+ subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -374,6 +415,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
it 'is sent as the merge author' do
sender = subject.header[:from].addrs[0]
@@ -399,9 +441,10 @@ describe Notify do
describe 'project was moved' do
let(:project) { create(:project) }
let(:user) { create(:user) }
- subject { Notify.project_was_moved_email(project.id, user.id) }
+ subject { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
it 'has the correct subject' do
is_expected.to have_subject /Project was moved/
@@ -423,13 +466,16 @@ describe Notify do
subject { Notify.project_access_granted_email(project_member.id) }
it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
it 'has the correct subject' do
is_expected.to have_subject /Access to project was granted/
end
+
it 'contains name of project' do
is_expected.to have_body_text /#{project.name}/
end
+
it 'contains new user role' do
is_expected.to have_body_text /#{project_member.human_access}/
end
@@ -444,6 +490,8 @@ describe Notify do
end
shared_examples 'a note email' do
+ it_behaves_like 'it should have Gmail Actions links'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(note_author.name)
@@ -467,7 +515,8 @@ describe Notify do
subject { Notify.note_commit_email(recipient.id, note.id) }
it_behaves_like 'a note email'
- it_behaves_like 'an answer to an existing thread', 'commits'
+ it_behaves_like 'an answer to an existing thread', 'commit'
+ it_behaves_like 'it should show Gmail Actions View Commit link'
it 'has the correct subject' do
is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/
@@ -487,6 +536,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
@@ -506,6 +556,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'it should show Gmail Actions View Issue link'
it 'has the correct subject' do
is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
@@ -526,6 +577,7 @@ describe Notify do
subject { Notify.group_access_granted_email(membership.id) }
it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
it 'has the correct subject' do
is_expected.to have_subject /Access to group was granted/
@@ -545,8 +597,10 @@ describe Notify do
let(:user) { create(:user, email: 'old-email@mail.com') }
before do
- user.email = "new-email@mail.com"
- user.save
+ perform_enqueued_jobs do
+ user.email = "new-email@mail.com"
+ user.save
+ end
end
subject { ActionMailer::Base.deliveries.last }
@@ -573,6 +627,8 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) }
+ it_behaves_like 'it should not have Gmail Actions links'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(user.name)
@@ -599,6 +655,8 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) }
+ it_behaves_like 'it should not have Gmail Actions links'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(user.name)
@@ -624,6 +682,8 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) }
+ it_behaves_like 'it should not have Gmail Actions links'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(user.name)
@@ -645,6 +705,8 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
+ it_behaves_like 'it should not have Gmail Actions links'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(user.name)
@@ -670,6 +732,8 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) }
+ it_behaves_like 'it should not have Gmail Actions links'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(user.name)
@@ -773,6 +837,8 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) }
+ it_behaves_like 'it should show Gmail Actions View Commit link'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(user.name)
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 635a6e2518c..d45319b25d4 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -16,4 +16,16 @@ RSpec.describe AbuseReport, type: :model do
subject { create(:abuse_report) }
it { expect(subject).to be_valid }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:reporter).class_name('User') }
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:reporter) }
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:message) }
+ it { is_expected.to validate_uniqueness_of(:user_id) }
+ end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index de0b2ef4cda..b67b84959d9 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -23,16 +23,36 @@
# after_sign_out_path :string(255)
# session_expire_delay :integer default(10080), not null
# import_sources :text
+# help_page_text :text
+# admin_notification_email :string(255)
+# shared_runners_enabled :boolean default(TRUE), not null
+# max_artifacts_size :integer default(100), not null
#
require 'spec_helper'
describe ApplicationSetting, models: true do
- it { expect(ApplicationSetting.create_from_defaults).to be_valid }
+ let(:setting) { ApplicationSetting.create_from_defaults }
- context 'restricted signup domains' do
- let(:setting) { ApplicationSetting.create_from_defaults }
+ it { expect(setting).to be_valid }
+
+ describe 'validations' do
+ let(:http) { 'http://example.com' }
+ let(:https) { 'https://example.com' }
+ let(:ftp) { 'ftp://example.com' }
+
+ it { is_expected.to allow_value(nil).for(:home_page_url) }
+ it { is_expected.to allow_value(http).for(:home_page_url) }
+ it { is_expected.to allow_value(https).for(:home_page_url) }
+ it { is_expected.not_to allow_value(ftp).for(:home_page_url) }
+
+ it { is_expected.to allow_value(nil).for(:after_sign_out_path) }
+ it { is_expected.to allow_value(http).for(:after_sign_out_path) }
+ it { is_expected.to allow_value(https).for(:after_sign_out_path) }
+ it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) }
+ end
+ context 'restricted signup domains' do
it 'set single domain' do
setting.restricted_signup_domains_raw = 'example.com'
expect(setting.restricted_signup_domains).to eq(['example.com'])
@@ -53,4 +73,26 @@ describe ApplicationSetting, models: true do
expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
end
end
+
+ context 'shared runners' do
+ let(:gl_project) { create(:empty_project) }
+
+ before do
+ allow_any_instance_of(Project).to receive(:current_application_settings).and_return(setting)
+ end
+
+ subject { gl_project.ensure_gitlab_ci_project.shared_runners_enabled }
+
+ context 'enabled' do
+ before { setting.update_attributes(shared_runners_enabled: true) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'disabled' do
+ before { setting.update_attributes(shared_runners_enabled: false) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 8ab72151a69..2b325f44f64 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -20,6 +20,21 @@ describe BroadcastMessage do
it { is_expected.to be_valid }
+ describe 'validations' do
+ let(:triplet) { '#000' }
+ let(:hex) { '#AABBCC' }
+
+ it { is_expected.to allow_value(nil).for(:color) }
+ it { is_expected.to allow_value(triplet).for(:color) }
+ it { is_expected.to allow_value(hex).for(:color) }
+ it { is_expected.not_to allow_value('000').for(:color) }
+
+ it { is_expected.to allow_value(nil).for(:font) }
+ it { is_expected.to allow_value(triplet).for(:font) }
+ it { is_expected.to allow_value(hex).for(:font) }
+ it { is_expected.not_to allow_value('000').for(:font) }
+ end
+
describe :current do
it "should return last message if time match" do
broadcast_message = create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow)
@@ -27,12 +42,12 @@ describe BroadcastMessage do
end
it "should return nil if time not come" do
- broadcast_message = create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days)
+ create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days)
expect(BroadcastMessage.current).to be_nil
end
it "should return nil if time has passed" do
- broadcast_message = create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday)
+ create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday)
expect(BroadcastMessage.current).to be_nil
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/build_spec.rb
index ce801152042..839b4c6b16e 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -27,16 +27,12 @@ require 'spec_helper'
describe Ci::Build do
let(:project) { FactoryGirl.create :ci_project }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
+ let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
let(:build) { FactoryGirl.create :ci_build, commit: commit }
- it { is_expected.to belong_to(:commit) }
- it { is_expected.to validate_presence_of :status }
+ it { is_expected.to validate_presence_of :ref }
- it { is_expected.to respond_to :success? }
- it { is_expected.to respond_to :failed? }
- it { is_expected.to respond_to :running? }
- it { is_expected.to respond_to :pending? }
it { is_expected.to respond_to :trace_html }
describe :first_pending do
@@ -63,72 +59,6 @@ describe Ci::Build do
end
end
- describe :started? do
- subject { build.started? }
-
- context 'without started_at' do
- before { build.started_at = nil }
-
- it { is_expected.to be_falsey }
- end
-
- %w(running success failed).each do |status|
- context "if build status is #{status}" do
- before { build.status = status }
-
- it { is_expected.to be_truthy }
- end
- end
-
- %w(pending canceled).each do |status|
- context "if build status is #{status}" do
- before { build.status = status }
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
- describe :active? do
- subject { build.active? }
-
- %w(pending running).each do |state|
- context "if build.status is #{state}" do
- before { build.status = state }
-
- it { is_expected.to be_truthy }
- end
- end
-
- %w(success failed canceled).each do |state|
- context "if build.status is #{state}" do
- before { build.status = state }
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
- describe :complete? do
- subject { build.complete? }
-
- %w(success failed canceled).each do |state|
- context "if build.status is #{state}" do
- before { build.status = state }
-
- it { is_expected.to be_truthy }
- end
- end
-
- %w(pending running).each do |state|
- context "if build.status is #{state}" do
- before { build.status = state }
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
describe :ignored? do
subject { build.ignored? }
@@ -177,37 +107,23 @@ describe Ci::Build do
it { is_expected.to include(text) }
it { expect(subject.length).to be >= text.length }
end
- end
-
- describe :timeout do
- subject { build.timeout }
-
- it { is_expected.to eq(commit.project.timeout) }
- end
-
- describe :duration do
- subject { build.duration }
- it { is_expected.to eq(120.0) }
+ context 'if build.trace hides token' do
+ let(:token) { 'my_secret_token' }
- context 'if the building process has not started yet' do
before do
- build.started_at = nil
- build.finished_at = nil
+ build.project.update_attributes(token: token)
+ build.update_attributes(trace: token)
end
- it { is_expected.to be_nil }
+ it { is_expected.to_not include(token) }
end
+ end
- context 'if the building process has started' do
- before do
- build.started_at = Time.now - 1.minute
- build.finished_at = nil
- end
+ describe :timeout do
+ subject { build.timeout }
- it { is_expected.to be_a(Float) }
- it { is_expected.to be > 0.0 }
- end
+ it { is_expected.to eq(commit.project.timeout) }
end
describe :options do
@@ -224,30 +140,6 @@ describe Ci::Build do
it { is_expected.to eq(options) }
end
- describe :ref do
- subject { build.ref }
-
- it { is_expected.to eq(commit.ref) }
- end
-
- describe :sha do
- subject { build.sha }
-
- it { is_expected.to eq(commit.sha) }
- end
-
- describe :short_sha do
- subject { build.short_sha }
-
- it { is_expected.to eq(commit.short_sha) }
- end
-
- describe :before_sha do
- subject { build.before_sha }
-
- it { is_expected.to eq(commit.before_sha) }
- end
-
describe :allow_git_fetch do
subject { build.allow_git_fetch }
@@ -308,13 +200,34 @@ describe Ci::Build do
context 'returns variables' do
subject { build.variables }
- let(:variables) do
+ let(:predefined_variables) do
+ [
+ { key: :CI_BUILD_NAME, value: 'test', public: true },
+ { key: :CI_BUILD_STAGE, value: 'stage', public: true },
+ ]
+ end
+
+ let(:yaml_variables) do
[
{ key: :DB_NAME, value: 'postgres', public: true }
]
end
- it { is_expected.to eq(variables) }
+ before { build.update_attributes(stage: 'stage') }
+
+ it { is_expected.to eq(predefined_variables + yaml_variables) }
+
+ context 'for tag' do
+ let(:tag_variable) do
+ [
+ { key: :CI_BUILD_TAG, value: 'master', public: true }
+ ]
+ end
+
+ before { build.update_attributes(tag: true) }
+
+ it { is_expected.to eq(tag_variable + predefined_variables + yaml_variables) }
+ end
context 'and secure variables' do
let(:secure_variables) do
@@ -327,7 +240,7 @@ describe Ci::Build do
build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
end
- it { is_expected.to eq(variables + secure_variables) }
+ it { is_expected.to eq(predefined_variables + yaml_variables + secure_variables) }
context 'and trigger variables' do
let(:trigger) { FactoryGirl.create :ci_trigger, project: project }
@@ -337,14 +250,169 @@ describe Ci::Build do
{ key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false }
]
end
+ let(:predefined_trigger_variable) do
+ [
+ { key: :CI_BUILD_TRIGGERED, value: 'true', public: true }
+ ]
+ end
before do
build.trigger_request = trigger_request
end
- it { is_expected.to eq(variables + secure_variables + trigger_variables) }
+ it { is_expected.to eq(predefined_variables + predefined_trigger_variable + yaml_variables + secure_variables + trigger_variables) }
+ end
+ end
+ end
+ end
+
+ describe :project_recipients do
+ let(:pusher_email) { 'pusher@gitlab.test' }
+ let(:user) { User.new(notification_email: pusher_email) }
+ subject { build.project_recipients }
+
+ before do
+ build.update_attributes(user: user)
+ end
+
+ it 'should return pusher_email as only recipient when no additional recipients are given' do
+ project.update_attributes(email_add_pusher: true,
+ email_recipients: '')
+ is_expected.to eq([pusher_email])
+ end
+
+ it 'should return pusher_email and additional recipients' do
+ project.update_attributes(email_add_pusher: true,
+ email_recipients: 'rec1 rec2')
+ is_expected.to eq(['rec1', 'rec2', pusher_email])
+ end
+
+ it 'should return recipients' do
+ project.update_attributes(email_add_pusher: false,
+ email_recipients: 'rec1 rec2')
+ is_expected.to eq(['rec1', 'rec2'])
+ end
+
+ it 'should return unique recipients only' do
+ project.update_attributes(email_add_pusher: true,
+ email_recipients: "rec1 rec1 #{pusher_email}")
+ is_expected.to eq(['rec1', pusher_email])
+ end
+ end
+
+ describe :can_be_served? do
+ let(:runner) { FactoryGirl.create :ci_specific_runner }
+
+ before { build.project.runners << runner }
+
+ context 'runner without tags' do
+ it 'can handle builds without tags' do
+ expect(build.can_be_served?(runner)).to be_truthy
+ end
+
+ it 'cannot handle build with tags' do
+ build.tag_list = ['aa']
+ expect(build.can_be_served?(runner)).to be_falsey
+ end
+ end
+
+ context 'runner with tags' do
+ before { runner.tag_list = ['bb', 'cc'] }
+
+ it 'can handle builds without tags' do
+ expect(build.can_be_served?(runner)).to be_truthy
+ end
+
+ it 'can handle build with matching tags' do
+ build.tag_list = ['bb']
+ expect(build.can_be_served?(runner)).to be_truthy
+ end
+
+ it 'cannot handle build with not matching tags' do
+ build.tag_list = ['aa']
+ expect(build.can_be_served?(runner)).to be_falsey
+ end
+ end
+ end
+
+ describe :any_runners_online? do
+ subject { build.any_runners_online? }
+
+ context 'when no runners' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'if there are runner' do
+ let(:runner) { FactoryGirl.create :ci_specific_runner }
+
+ before do
+ build.project.runners << runner
+ runner.update_attributes(contacted_at: 1.second.ago)
+ end
+
+ it { is_expected.to be_truthy }
+
+ it 'that is inactive' do
+ runner.update_attributes(active: false)
+ is_expected.to be_falsey
+ end
+
+ it 'that is not online' do
+ runner.update_attributes(contacted_at: nil)
+ is_expected.to be_falsey
+ end
+
+ it 'that cannot handle build' do
+ expect_any_instance_of(Ci::Build).to receive(:can_be_served?).and_return(false)
+ is_expected.to be_falsey
+ end
+
+ end
+ end
+
+ describe :show_warning? do
+ subject { build.show_warning? }
+
+ %w(pending).each do |state|
+ context "if commit_status.status is #{state}" do
+ before { build.status = state }
+
+ it { is_expected.to be_truthy }
+
+ context "and there are specific runner" do
+ let(:runner) { FactoryGirl.create :ci_specific_runner, contacted_at: 1.second.ago }
+
+ before do
+ build.project.runners << runner
+ runner.save
+ end
+
+ it { is_expected.to be_falsey }
end
end
end
+
+ %w(success failed canceled running).each do |state|
+ context "if commit_status.status is #{state}" do
+ before { build.status = state }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe :download_url do
+ subject { build.download_url }
+
+ it "should be nil if artifact doesn't exist" do
+ build.update_attributes(artifacts_file: nil)
+ is_expected.to be_nil
+ end
+
+ it 'should be nil if artifact exist' do
+ gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
+ build.update_attributes(artifacts_file: gif)
+ is_expected.to_not be_nil
+ end
end
end
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
index 586c9dc23a7..a13f6458cac 100644
--- a/spec/models/ci/commit_spec.rb
+++ b/spec/models/ci/commit_spec.rb
@@ -1,39 +1,56 @@
# == Schema Information
#
-# Table name: commits
+# Table name: ci_commits
#
-# id :integer not null, primary key
-# project_id :integer
-# ref :string(255)
-# sha :string(255)
-# before_sha :string(255)
-# push_data :text
-# created_at :datetime
-# updated_at :datetime
-# tag :boolean default(FALSE)
-# yaml_errors :text
-# committed_at :datetime
+# id :integer not null, primary key
+# project_id :integer
+# ref :string(255)
+# sha :string(255)
+# before_sha :string(255)
+# push_data :text
+# created_at :datetime
+# updated_at :datetime
+# tag :boolean default(FALSE)
+# yaml_errors :text
+# committed_at :datetime
+# gl_project_id :integer
#
require 'spec_helper'
describe Ci::Commit do
let(:project) { FactoryGirl.create :ci_project }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
- let(:commit_with_project) { FactoryGirl.create :ci_commit, project: project }
- let(:config_processor) { Ci::GitlabCiYamlProcessor.new(gitlab_ci_yaml) }
+ let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
+ let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
- it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:gl_project) }
+ it { is_expected.to have_many(:statuses) }
+ it { is_expected.to have_many(:trigger_requests) }
it { is_expected.to have_many(:builds) }
- it { is_expected.to validate_presence_of :before_sha }
it { is_expected.to validate_presence_of :sha }
- it { is_expected.to validate_presence_of :ref }
- it { is_expected.to validate_presence_of :push_data }
it { is_expected.to respond_to :git_author_name }
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
+ describe :ordered do
+ let(:project) { FactoryGirl.create :empty_project }
+
+ it 'returns ordered list of commits' do
+ commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project
+ commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project
+ expect(project.ci_commits.ordered).to eq([commit2, commit1])
+ end
+
+ it 'returns commits ordered by committed_at and id, with nulls last' do
+ commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project
+ commit2 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project
+ commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project
+ commit4 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project
+ expect(project.ci_commits.ordered).to eq([commit2, commit4, commit3, commit1])
+ end
+ end
+
describe :last_build do
subject { commit.last_build }
before do
@@ -51,53 +68,12 @@ describe Ci::Commit do
@second = FactoryGirl.create :ci_build, commit: commit
end
- it "creates new build" do
+ it "creates only a new build" do
expect(commit.builds.count(:all)).to eq 2
+ expect(commit.statuses.count(:all)).to eq 2
commit.retry
expect(commit.builds.count(:all)).to eq 3
- end
- end
-
- describe :project_recipients do
-
- context 'always sending notification' do
- it 'should return commit_pusher_email as only recipient when no additional recipients are given' do
- project = FactoryGirl.create :ci_project,
- email_add_pusher: true,
- email_recipients: ''
- commit = FactoryGirl.create :ci_commit, project: project
- expected = 'commit_pusher_email'
- allow(commit).to receive(:push_data) { { user_email: expected } }
- expect(commit.project_recipients).to eq([expected])
- end
-
- it 'should return commit_pusher_email and additional recipients' do
- project = FactoryGirl.create :ci_project,
- email_add_pusher: true,
- email_recipients: 'rec1 rec2'
- commit = FactoryGirl.create :ci_commit, project: project
- expected = 'commit_pusher_email'
- allow(commit).to receive(:push_data) { { user_email: expected } }
- expect(commit.project_recipients).to eq(['rec1', 'rec2', expected])
- end
-
- it 'should return recipients' do
- project = FactoryGirl.create :ci_project,
- email_add_pusher: false,
- email_recipients: 'rec1 rec2'
- commit = FactoryGirl.create :ci_commit, project: project
- expect(commit.project_recipients).to eq(['rec1', 'rec2'])
- end
-
- it 'should return unique recipients only' do
- project = FactoryGirl.create :ci_project,
- email_add_pusher: true,
- email_recipients: 'rec1 rec1 rec2'
- commit = FactoryGirl.create :ci_commit, project: project
- expected = 'rec2'
- allow(commit).to receive(:push_data) { { user_email: expected } }
- expect(commit.project_recipients).to eq(['rec1', 'rec2'])
- end
+ expect(commit.statuses.count(:all)).to eq 3
end
end
@@ -112,63 +88,122 @@ describe Ci::Commit do
end
end
- describe :compare? do
- subject { commit_with_project.compare? }
-
- context 'if commit.before_sha are not nil' do
- it { is_expected.to be_truthy }
- end
- end
-
describe :short_sha do
- subject { commit.short_before_sha }
+ subject { commit.short_sha }
it 'has 8 items' do
expect(subject.size).to eq(8)
end
- it { expect(commit.before_sha).to start_with(subject) }
+ it { expect(commit.sha).to start_with(subject) }
end
- describe :short_sha do
- subject { commit.short_sha }
+ describe :stage do
+ subject { commit.stage }
- it 'has 8 items' do
- expect(subject.size).to eq(8)
+ before do
+ @second = FactoryGirl.create :commit_status, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: 'pending'
+ @first = FactoryGirl.create :commit_status, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: 'pending'
+ end
+
+ it 'returns first running stage' do
+ is_expected.to eq('test')
+ end
+
+ context 'first build succeeded' do
+ before do
+ @first.success
+ end
+
+ it 'returns last running stage' do
+ is_expected.to eq('deploy')
+ end
+ end
+
+ context 'all builds succeeded' do
+ before do
+ @first.success
+ @second.success
+ end
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
end
- it { expect(commit.sha).to start_with(subject) }
end
describe :create_next_builds do
+ end
+
+ describe :refs do
+ subject { commit.refs }
+
before do
- allow(commit).to receive(:config_processor).and_return(config_processor)
+ FactoryGirl.create :commit_status, commit: commit, name: 'deploy'
+ FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'develop'
+ FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'master'
end
- it "creates builds for next type" do
- expect(commit.create_builds).to be_truthy
- commit.builds.reload
- expect(commit.builds.size).to eq(2)
+ it 'returns all refs' do
+ is_expected.to contain_exactly('master', 'develop', nil)
+ end
+ end
- expect(commit.create_next_builds(nil)).to be_truthy
- commit.builds.reload
- expect(commit.builds.size).to eq(4)
+ describe :retried do
+ subject { commit.retried }
- expect(commit.create_next_builds(nil)).to be_truthy
- commit.builds.reload
- expect(commit.builds.size).to eq(5)
+ before do
+ @commit1 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy'
+ @commit2 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy'
+ end
- expect(commit.create_next_builds(nil)).to be_falsey
+ it 'returns old builds' do
+ is_expected.to contain_exactly(@commit1)
end
end
describe :create_builds do
- before do
- allow(commit).to receive(:config_processor).and_return(config_processor)
+ let!(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+
+ def create_builds(trigger_request = nil)
+ commit.create_builds('master', false, nil, trigger_request)
+ end
+
+ def create_next_builds
+ commit.create_next_builds(commit.builds.order(:id).last)
end
it 'creates builds' do
- expect(commit.create_builds).to be_truthy
- commit.builds.reload
- expect(commit.builds.size).to eq(2)
+ expect(create_builds).to be_truthy
+ commit.builds.update_all(status: "success")
+ expect(commit.builds.count(:all)).to eq(2)
+
+ expect(create_next_builds).to be_truthy
+ commit.builds.update_all(status: "success")
+ expect(commit.builds.count(:all)).to eq(4)
+
+ expect(create_next_builds).to be_truthy
+ commit.builds.update_all(status: "success")
+ expect(commit.builds.count(:all)).to eq(5)
+
+ expect(create_next_builds).to be_falsey
+ end
+
+ context 'for different ref' do
+ def create_develop_builds
+ commit.create_builds('develop', false, nil, nil)
+ end
+
+ it 'creates builds' do
+ expect(create_builds).to be_truthy
+ commit.builds.update_all(status: "success")
+ expect(commit.builds.count(:all)).to eq(2)
+
+ expect(create_develop_builds).to be_truthy
+ commit.builds.update_all(status: "success")
+ expect(commit.builds.count(:all)).to eq(4)
+ expect(commit.refs.size).to eq(2)
+ expect(commit.builds.pluck(:name).uniq.size).to eq(2)
+ end
end
context 'for build triggers' do
@@ -176,61 +211,179 @@ describe Ci::Commit do
let(:trigger_request) { FactoryGirl.create :ci_trigger_request, commit: commit, trigger: trigger }
it 'creates builds' do
- expect(commit.create_builds(trigger_request)).to be_truthy
- commit.builds.reload
- expect(commit.builds.size).to eq(2)
+ expect(create_builds(trigger_request)).to be_truthy
+ expect(commit.builds.count(:all)).to eq(2)
end
it 'rebuilds commit' do
- expect(commit.create_builds).to be_truthy
- commit.builds.reload
- expect(commit.builds.size).to eq(2)
+ expect(create_builds).to be_truthy
+ expect(commit.builds.count(:all)).to eq(2)
- expect(commit.create_builds(trigger_request)).to be_truthy
- commit.builds.reload
- expect(commit.builds.size).to eq(4)
+ expect(create_builds(trigger_request)).to be_truthy
+ expect(commit.builds.count(:all)).to eq(4)
end
it 'creates next builds' do
- expect(commit.create_builds(trigger_request)).to be_truthy
- commit.builds.reload
- expect(commit.builds.size).to eq(2)
+ expect(create_builds(trigger_request)).to be_truthy
+ expect(commit.builds.count(:all)).to eq(2)
+ commit.builds.update_all(status: "success")
- expect(commit.create_next_builds(trigger_request)).to be_truthy
- commit.builds.reload
- expect(commit.builds.size).to eq(4)
+ expect(create_next_builds).to be_truthy
+ expect(commit.builds.count(:all)).to eq(4)
end
context 'for [ci skip]' do
before do
- commit.push_data[:commits][0][:message] = 'skip this commit [ci skip]'
- commit.save
+ allow(commit).to receive(:git_commit_message) { 'message [ci skip]' }
end
it 'rebuilds commit' do
expect(commit.status).to eq('skipped')
- expect(commit.create_builds(trigger_request)).to be_truthy
- commit.builds.reload
- expect(commit.builds.size).to eq(2)
- expect(commit.status).to eq('pending')
+ expect(create_builds).to be_truthy
+
+ # since everything in Ci::Commit is cached we need to fetch a new object
+ new_commit = Ci::Commit.find_by_id(commit.id)
+ expect(new_commit.status).to eq('pending')
end
end
end
+
+ context 'properly creates builds when "when" is defined' do
+ let(:yaml) do
+ {
+ stages: ["build", "test", "test_failure", "deploy", "cleanup"],
+ build: {
+ stage: "build",
+ script: "BUILD",
+ },
+ test: {
+ stage: "test",
+ script: "TEST",
+ },
+ test_failure: {
+ stage: "test_failure",
+ script: "ON test failure",
+ when: "on_failure",
+ },
+ deploy: {
+ stage: "deploy",
+ script: "PUBLISH",
+ },
+ cleanup: {
+ stage: "cleanup",
+ script: "TIDY UP",
+ when: "always",
+ }
+ }
+ end
+
+ before do
+ stub_ci_commit_yaml_file(YAML.dump(yaml))
+ end
+
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(commit.builds.pluck(:name)).to contain_exactly('build')
+ expect(commit.builds.pluck(:status)).to contain_exactly('pending')
+ commit.builds.running_or_pending.each(&:success)
+
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ commit.builds.running_or_pending.each(&:success)
+
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+ commit.builds.running_or_pending.each(&:success)
+
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
+ commit.builds.running_or_pending.each(&:success)
+
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
+ expect(commit.status).to eq('success')
+ end
+
+ it 'properly creates builds when test fails' do
+ expect(create_builds).to be_truthy
+ expect(commit.builds.pluck(:name)).to contain_exactly('build')
+ expect(commit.builds.pluck(:status)).to contain_exactly('pending')
+ commit.builds.running_or_pending.each(&:success)
+
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ commit.builds.running_or_pending.each(&:drop)
+
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+ commit.builds.running_or_pending.each(&:success)
+
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
+ commit.builds.running_or_pending.each(&:success)
+
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
+ expect(commit.status).to eq('failed')
+ end
+
+ it 'properly creates builds when test and test_failure fails' do
+ expect(create_builds).to be_truthy
+ expect(commit.builds.pluck(:name)).to contain_exactly('build')
+ expect(commit.builds.pluck(:status)).to contain_exactly('pending')
+ commit.builds.running_or_pending.each(&:success)
+
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ commit.builds.running_or_pending.each(&:drop)
+
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+ commit.builds.running_or_pending.each(&:drop)
+
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
+ commit.builds.running_or_pending.each(&:success)
+
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
+ expect(commit.status).to eq('failed')
+ end
+
+ it 'properly creates builds when deploy fails' do
+ expect(create_builds).to be_truthy
+ expect(commit.builds.pluck(:name)).to contain_exactly('build')
+ expect(commit.builds.pluck(:status)).to contain_exactly('pending')
+ commit.builds.running_or_pending.each(&:success)
+
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ commit.builds.running_or_pending.each(&:success)
+
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+ commit.builds.running_or_pending.each(&:drop)
+
+ expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
+ commit.builds.running_or_pending.each(&:success)
+
+ expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
+ expect(commit.status).to eq('failed')
+ end
+ end
end
describe "#finished_at" do
- let(:project) { FactoryGirl.create :ci_project }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:commit) { FactoryGirl.create :ci_commit }
it "returns finished_at of latest build" do
build = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 60
- build1 = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 120
+ FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 120
expect(commit.finished_at.to_i).to eq(build.finished_at.to_i)
end
it "returns nil if there is no finished build" do
- build = FactoryGirl.create :ci_not_started_build, commit: commit
+ FactoryGirl.create :ci_not_started_build, commit: commit
expect(commit.finished_at).to be_nil
end
@@ -238,7 +391,8 @@ describe Ci::Commit do
describe "coverage" do
let(:project) { FactoryGirl.create :ci_project, coverage_regex: "/.*/" }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
+ let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
it "calculates average when there are two builds with coverage" do
FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
diff --git a/spec/models/ci/project_services/hip_chat_message_spec.rb b/spec/models/ci/project_services/hip_chat_message_spec.rb
index 49ac0860259..e23d6ae2c28 100644
--- a/spec/models/ci/project_services/hip_chat_message_spec.rb
+++ b/spec/models/ci/project_services/hip_chat_message_spec.rb
@@ -3,72 +3,37 @@ require 'spec_helper'
describe Ci::HipChatMessage do
subject { Ci::HipChatMessage.new(build) }
- let(:project) { FactoryGirl.create(:ci_project) }
+ let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) }
- context "One build" do
- let(:commit) { FactoryGirl.create(:ci_commit_with_one_job, project: project) }
-
- let(:build) do
- commit.create_builds
- commit.builds.first
- end
-
- context 'when build succeeds' do
- it 'returns a successful message' do
- build.update(status: "success")
-
- expect( subject.status_color ).to eq 'green'
- expect( subject.notify? ).to be_falsey
- expect( subject.to_s ).to match(/Build '[^']+' #\d+/)
- expect( subject.to_s ).to match(/Successful in \d+ second\(s\)\./)
- end
- end
-
- context 'when build fails' do
- it 'returns a failure message' do
- build.update(status: "failed")
-
- expect( subject.status_color ).to eq 'red'
- expect( subject.notify? ).to be_truthy
- expect( subject.to_s ).to match(/Build '[^']+' #\d+/)
- expect( subject.to_s ).to match(/Failed in \d+ second\(s\)\./)
- end
- end
+ let(:build) do
+ commit.builds.first
end
- context "Several builds" do
- let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs, project: project) }
-
- let(:build) do
- commit.builds.first
- end
-
- context 'when all matrix builds succeed' do
- it 'returns a successful message' do
- commit.create_builds
- commit.builds.update_all(status: "success")
- commit.reload
+ context 'when all matrix builds succeed' do
+ it 'returns a successful message' do
+ commit.create_builds('master', false, nil)
+ commit.builds.update_all(status: "success")
+ commit.reload
- expect( subject.status_color ).to eq 'green'
- expect( subject.notify? ).to be_falsey
- expect( subject.to_s ).to match(/Commit #\d+/)
- expect( subject.to_s ).to match(/Successful in \d+ second\(s\)\./)
- end
+ expect(subject.status_color).to eq 'green'
+ expect(subject.notify?).to be_falsey
+ expect(subject.to_s).to match(/Commit #\d+/)
+ expect(subject.to_s).to match(/Successful in \d+ second\(s\)\./)
end
+ end
- context 'when at least one matrix build fails' do
- it 'returns a failure message' do
- commit.create_builds
- first_build = commit.builds.first
- second_build = commit.builds.last
- first_build.update(status: "success")
- second_build.update(status: "failed")
-
- expect( subject.status_color ).to eq 'red'
- expect( subject.notify? ).to be_truthy
- expect( subject.to_s ).to match(/Commit #\d+/)
- expect( subject.to_s ).to match(/Failed in \d+ second\(s\)\./)
- end
+ context 'when at least one matrix build fails' do
+ it 'returns a failure message' do
+ commit.create_builds('master', false, nil)
+ first_build = commit.builds.first
+ second_build = commit.builds.last
+ first_build.update(status: "success")
+ second_build.update(status: "failed")
+
+ expect(subject.status_color).to eq 'red'
+ expect(subject.notify?).to be_truthy
+ expect(subject.to_s).to match(/Commit #\d+/)
+ expect(subject.to_s).to match(/Failed in \d+ second\(s\)\./)
end
end
end
diff --git a/spec/models/ci/project_services/hip_chat_service_spec.rb b/spec/models/ci/project_services/hip_chat_service_spec.rb
index 063d46b84d4..d9ccc855edf 100644
--- a/spec/models/ci/project_services/hip_chat_service_spec.rb
+++ b/spec/models/ci/project_services/hip_chat_service_spec.rb
@@ -33,15 +33,14 @@ describe Ci::HipChatService do
describe "Execute" do
let(:service) { Ci::HipChatService.new }
- let(:project) { FactoryGirl.create :ci_project }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:commit) { FactoryGirl.create :ci_commit }
let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' }
let(:api_url) { 'https://api.hipchat.com/v2/room/123/notification?auth_token=a1b2c3d4e5f6' }
before do
allow(service).to receive_messages(
- project: project,
- project_id: project.id,
+ project: commit.project,
+ project_id: commit.project_id,
notify_only_broken_builds: false,
hipchat_room: 123,
hipchat_token: 'a1b2c3d4e5f6'
diff --git a/spec/models/ci/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb
index b5f37b349db..c03be3ef75f 100644
--- a/spec/models/ci/mail_service_spec.rb
+++ b/spec/models/ci/project_services/mail_service_spec.rb
@@ -29,11 +29,13 @@ describe Ci::MailService do
describe 'Sends email for' do
let(:mail) { Ci::MailService.new }
+ let(:user) { User.new(notification_email: 'git@example.com')}
describe 'failed build' do
let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true) }
- let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
- let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit) }
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) }
before do
allow(mail).to receive_messages(
@@ -42,20 +44,18 @@ describe Ci::MailService do
end
it do
- should_email("git@example.com")
- mail.execute(build)
- end
-
- def should_email(email)
- expect(Ci::Notify).to receive(:build_fail_email).with(build.id, email)
- expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email)
+ perform_enqueued_jobs do
+ expect{ mail.execute(build) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
+ expect(ActionMailer::Base.deliveries.last.to).to eq(["git@example.com"])
+ end
end
end
describe 'successfull build' do
let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true, email_only_broken_builds: false) }
- let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
- let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) }
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
before do
allow(mail).to receive_messages(
@@ -64,13 +64,10 @@ describe Ci::MailService do
end
it do
- should_email("git@example.com")
- mail.execute(build)
- end
-
- def should_email(email)
- expect(Ci::Notify).to receive(:build_success_email).with(build.id, email)
- expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
+ perform_enqueued_jobs do
+ expect{ mail.execute(build) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
+ expect(ActionMailer::Base.deliveries.last.to).to eq(["git@example.com"])
+ end
end
end
@@ -81,8 +78,9 @@ describe Ci::MailService do
email_only_broken_builds: false,
email_recipients: "jeroen@example.com")
end
- let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
- let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) }
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
before do
allow(mail).to receive_messages(
@@ -91,14 +89,12 @@ describe Ci::MailService do
end
it do
- should_email("git@example.com")
- should_email("jeroen@example.com")
- mail.execute(build)
- end
-
- def should_email(email)
- expect(Ci::Notify).to receive(:build_success_email).with(build.id, email)
- expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
+ perform_enqueued_jobs do
+ expect{ mail.execute(build) }.to change{ ActionMailer::Base.deliveries.size }.by(2)
+ expect(
+ ActionMailer::Base.deliveries.map(&:to).flatten
+ ).to include("git@example.com", "jeroen@example.com")
+ end
end
end
@@ -109,8 +105,9 @@ describe Ci::MailService do
email_only_broken_builds: true,
email_recipients: "jeroen@example.com")
end
- let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
- let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) }
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
before do
allow(mail).to receive_messages(
@@ -119,14 +116,11 @@ describe Ci::MailService do
end
it do
- should_email(commit.git_author_email)
- should_email("jeroen@example.com")
- mail.execute(build) if mail.can_execute?(build)
- end
-
- def should_email(email)
- expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email)
- expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
+ perform_enqueued_jobs do
+ expect do
+ mail.execute(build) if mail.can_execute?(build)
+ end.to_not change{ ActionMailer::Base.deliveries.size }
+ end
end
end
@@ -137,8 +131,9 @@ describe Ci::MailService do
email_only_broken_builds: false,
email_recipients: "jeroen@example.com")
end
- let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
- let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) }
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
before do
allow(mail).to receive_messages(
@@ -159,8 +154,9 @@ describe Ci::MailService do
email_only_broken_builds: true,
email_recipients: "jeroen@example.com")
end
- let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
- let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit) }
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) }
before do
allow(mail).to receive_messages(
@@ -170,14 +166,11 @@ describe Ci::MailService do
it do
Ci::Build.retry(build)
- should_email(commit.git_author_email)
- should_email("jeroen@example.com")
- mail.execute(build) if mail.can_execute?(build)
- end
-
- def should_email(email)
- expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email)
- expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
+ perform_enqueued_jobs do
+ expect do
+ mail.execute(build) if mail.can_execute?(build)
+ end.to_not change{ ActionMailer::Base.deliveries.size }
+ end
end
end
end
diff --git a/spec/models/ci/project_services/slack_message_spec.rb b/spec/models/ci/project_services/slack_message_spec.rb
index f5335903728..8adda6c86cc 100644
--- a/spec/models/ci/project_services/slack_message_spec.rb
+++ b/spec/models/ci/project_services/slack_message_spec.rb
@@ -3,82 +3,41 @@ require 'spec_helper'
describe Ci::SlackMessage do
subject { Ci::SlackMessage.new(commit) }
- let(:project) { FactoryGirl.create :ci_project }
+ let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) }
- context "One build" do
- let(:commit) { FactoryGirl.create(:ci_commit_with_one_job, project: project) }
+ context 'when all matrix builds succeeded' do
+ let(:color) { 'good' }
- let(:build) do
- commit.create_builds
- commit.builds.first
- end
-
- context 'when build succeeded' do
- let(:color) { 'good' }
-
- it 'returns a message with succeeded build' do
- build.update(status: "success")
-
- expect(subject.color).to eq(color)
- expect(subject.fallback).to include('Build')
- expect(subject.fallback).to include("\##{build.id}")
- expect(subject.fallback).to include('succeeded')
- expect(subject.attachments.first[:fields]).to be_empty
- end
- end
-
- context 'when build failed' do
- let(:color) { 'danger' }
-
- it 'returns a message with failed build' do
- build.update(status: "failed")
+ it 'returns a message with success' do
+ commit.create_builds('master', false, nil)
+ commit.builds.update_all(status: "success")
+ commit.reload
- expect(subject.color).to eq(color)
- expect(subject.fallback).to include('Build')
- expect(subject.fallback).to include("\##{build.id}")
- expect(subject.fallback).to include('failed')
- expect(subject.attachments.first[:fields]).to be_empty
- end
+ expect(subject.color).to eq(color)
+ expect(subject.fallback).to include('Commit')
+ expect(subject.fallback).to include("\##{commit.id}")
+ expect(subject.fallback).to include('succeeded')
+ expect(subject.attachments.first[:fields]).to be_empty
end
end
- context "Several builds" do
- let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs, project: project) }
-
- context 'when all matrix builds succeeded' do
- let(:color) { 'good' }
-
- it 'returns a message with success' do
- commit.create_builds
- commit.builds.update_all(status: "success")
- commit.reload
-
- expect(subject.color).to eq(color)
- expect(subject.fallback).to include('Commit')
- expect(subject.fallback).to include("\##{commit.id}")
- expect(subject.fallback).to include('succeeded')
- expect(subject.attachments.first[:fields]).to be_empty
- end
- end
-
- context 'when one of matrix builds failed' do
- let(:color) { 'danger' }
-
- it 'returns a message with information about failed build' do
- commit.create_builds
- first_build = commit.builds.first
- second_build = commit.builds.last
- first_build.update(status: "success")
- second_build.update(status: "failed")
-
- expect(subject.color).to eq(color)
- expect(subject.fallback).to include('Commit')
- expect(subject.fallback).to include("\##{commit.id}")
- expect(subject.fallback).to include('failed')
- expect(subject.attachments.first[:fields].size).to eq(1)
- expect(subject.attachments.first[:fields].first[:title]).to eq(second_build.name)
- expect(subject.attachments.first[:fields].first[:value]).to include("\##{second_build.id}")
- end
+ context 'when one of matrix builds failed' do
+ let(:color) { 'danger' }
+
+ it 'returns a message with information about failed build' do
+ commit.create_builds('master', false, nil)
+ first_build = commit.builds.first
+ second_build = commit.builds.last
+ first_build.update(status: "success")
+ second_build.update(status: "failed")
+
+ expect(subject.color).to eq(color)
+ expect(subject.fallback).to include('Commit')
+ expect(subject.fallback).to include("\##{commit.id}")
+ expect(subject.fallback).to include('failed')
+ expect(subject.attachments.first[:fields].size).to eq(1)
+ expect(subject.attachments.first[:fields].first[:title]).to eq(second_build.name)
+ expect(subject.attachments.first[:fields].first[:value]).to include("\##{second_build.id}")
end
end
end
diff --git a/spec/models/ci/project_services/slack_service_spec.rb b/spec/models/ci/project_services/slack_service_spec.rb
index 0524f472432..1ac7dfe568d 100644
--- a/spec/models/ci/project_services/slack_service_spec.rb
+++ b/spec/models/ci/project_services/slack_service_spec.rb
@@ -31,16 +31,15 @@ describe Ci::SlackService do
describe "Execute" do
let(:slack) { Ci::SlackService.new }
- let(:project) { FactoryGirl.create :ci_project }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:commit) { FactoryGirl.create :ci_commit }
let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' }
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
let(:notify_only_broken_builds) { false }
before do
allow(slack).to receive_messages(
- project: project,
- project_id: project.id,
+ project: commit.project,
+ project_id: commit.project_id,
webhook: webhook_url,
notify_only_broken_builds: notify_only_broken_builds
)
diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb
index 261ea69f5b4..ac7e38bbcb0 100644
--- a/spec/models/ci/project_spec.rb
+++ b/spec/models/ci/project_spec.rb
@@ -1,9 +1,9 @@
# == Schema Information
#
-# Table name: projects
+# Table name: ci_projects
#
# id :integer not null, primary key
-# name :string(255) not null
+# name :string(255)
# timeout :integer default(3600), not null
# created_at :datetime
# updated_at :datetime
@@ -28,13 +28,20 @@
require 'spec_helper'
describe Ci::Project do
- subject { FactoryGirl.build :ci_project }
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:gl_project) { project.gl_project }
+ subject { project }
+
+ it { is_expected.to have_many(:runner_projects) }
+ it { is_expected.to have_many(:runners) }
+ it { is_expected.to have_many(:web_hooks) }
+ it { is_expected.to have_many(:events) }
+ it { is_expected.to have_many(:variables) }
+ it { is_expected.to have_many(:triggers) }
+ it { is_expected.to have_many(:services) }
- it { is_expected.to have_many(:commits) }
-
- it { is_expected.to validate_presence_of :name }
it { is_expected.to validate_presence_of :timeout }
- it { is_expected.to validate_presence_of :default_ref }
+ it { is_expected.to validate_presence_of :gitlab_id }
describe 'before_validation' do
it 'should set an random token if none provided' do
@@ -48,43 +55,89 @@ describe Ci::Project do
end
end
- describe "ordered_by_last_commit_date" do
- it "returns ordered projects" do
- newest_project = FactoryGirl.create :ci_project
- oldest_project = FactoryGirl.create :ci_project
- project_without_commits = FactoryGirl.create :ci_project
+ describe :name_with_namespace do
+ subject { project.name_with_namespace }
+
+ it { is_expected.to eq(project.name) }
+ it { is_expected.to eq(gl_project.name_with_namespace) }
+ end
+
+ describe :path_with_namespace do
+ subject { project.path_with_namespace }
+
+ it { is_expected.to eq(project.path) }
+ it { is_expected.to eq(gl_project.path_with_namespace) }
+ end
+
+ describe :path_with_namespace do
+ subject { project.web_url }
+
+ it { is_expected.to eq(gl_project.web_url) }
+ end
- FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: newest_project
- FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: oldest_project
+ describe :web_url do
+ subject { project.web_url }
- expect(Ci::Project.ordered_by_last_commit_date).to eq([newest_project, oldest_project, project_without_commits])
+ it { is_expected.to eq(project.gitlab_url) }
+ it { is_expected.to eq(gl_project.web_url) }
+ end
+
+ describe :http_url_to_repo do
+ subject { project.http_url_to_repo }
+
+ it { is_expected.to eq(gl_project.http_url_to_repo) }
+ end
+
+ describe :ssh_url_to_repo do
+ subject { project.ssh_url_to_repo }
+
+ it { is_expected.to eq(gl_project.ssh_url_to_repo) }
+ end
+
+ describe :commits do
+ subject { project.commits }
+
+ before do
+ FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project
end
+
+ it { is_expected.to eq(gl_project.ci_commits) }
end
- describe 'ordered commits' do
- let(:project) { FactoryGirl.create :ci_project }
+ describe :builds do
+ subject { project.builds }
- it 'returns ordered list of commits' do
- commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
- commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project
- expect(project.commits).to eq([commit2, commit1])
+ before do
+ commit = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project
+ FactoryGirl.create :ci_build, commit: commit
end
- it 'returns commits ordered by committed_at and id, with nulls last' do
- commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
- commit2 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
- commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project
- commit4 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
- expect(project.commits).to eq([commit2, commit4, commit3, commit1])
+ it { is_expected.to eq(gl_project.ci_builds) }
+ end
+
+ describe "ordered_by_last_commit_date" do
+ it "returns ordered projects" do
+ newest_project = FactoryGirl.create :empty_project
+ newest_ci_project = newest_project.ensure_gitlab_ci_project
+ oldest_project = FactoryGirl.create :empty_project
+ oldest_ci_project = oldest_project.ensure_gitlab_ci_project
+ project_without_commits = FactoryGirl.create :empty_project
+ ci_project_without_commits = project_without_commits.ensure_gitlab_ci_project
+
+ FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: newest_project
+ FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: oldest_project
+
+ expect(Ci::Project.ordered_by_last_commit_date).to eq([newest_ci_project, oldest_ci_project, ci_project_without_commits])
end
end
context :valid_project do
- let(:project) { FactoryGirl.create :ci_project }
+ let(:commit) { FactoryGirl.create(:ci_commit) }
context :project_with_commit_and_builds do
+ let(:project) { commit.project }
+
before do
- commit = FactoryGirl.create(:ci_commit, project: project)
FactoryGirl.create(:ci_build, commit: commit)
end
@@ -141,18 +194,6 @@ describe Ci::Project do
end
end
- describe 'Project.parse' do
- let(:project) { FactoryGirl.create :project }
-
- subject { Ci::Project.parse(project) }
-
- it { is_expected.to be_valid }
- it { is_expected.to be_kind_of(Ci::Project) }
- it { expect(subject.name).to eq(project.name_with_namespace) }
- it { expect(subject.gitlab_id).to eq(project.id) }
- it { expect(subject.gitlab_url).to eq(project.web_url) }
- end
-
describe :repo_url_with_auth do
let(:project) { FactoryGirl.create :ci_project }
subject { project.repo_url_with_auth }
@@ -165,13 +206,6 @@ describe Ci::Project do
it { is_expected.to include(project.gitlab_url[7..-1]) }
end
- describe :search do
- let!(:project) { FactoryGirl.create(:ci_project, name: "foo") }
-
- it { expect(Ci::Project.search('fo')).to include(project) }
- it { expect(Ci::Project.search('bar')).to be_empty }
- end
-
describe :any_runners do
it "there are no runners available" do
project = FactoryGirl.create(:ci_project)
@@ -195,5 +229,18 @@ describe Ci::Project do
FactoryGirl.create(:ci_shared_runner)
expect(project.any_runners?).to be_falsey
end
+
+ it "checks the presence of specific runner" do
+ project = FactoryGirl.create(:ci_project)
+ specific_runner = FactoryGirl.create(:ci_specific_runner)
+ project.runners << specific_runner
+ expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
+ end
+
+ it "checks the presence of shared runner" do
+ project = FactoryGirl.create(:ci_project, shared_runners_enabled: true)
+ shared_runner = FactoryGirl.create(:ci_shared_runner)
+ expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy
+ end
end
end
diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb
index 0218d484130..37682c6ea0c 100644
--- a/spec/models/ci/runner_project_spec.rb
+++ b/spec/models/ci/runner_project_spec.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: runner_projects
+# Table name: ci_runner_projects
#
# id :integer not null, primary key
# runner_id :integer not null
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 757593a7ab8..9a1233b9095 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: runners
+# Table name: ci_runners
#
# id :integer not null, primary key
# token :string(255)
@@ -32,7 +32,7 @@ describe Ci::Runner do
end
it 'should return the token if the description is an empty string' do
- runner = FactoryGirl.build(:ci_runner, description: '')
+ runner = FactoryGirl.build(:ci_runner, description: '', token: 'token')
expect(runner.display_name).to eq runner.token
end
end
@@ -48,6 +48,71 @@ describe Ci::Runner do
it { expect(shared_runner.only_for?(project)).to be_truthy }
end
+ describe :online do
+ subject { Ci::Runner.online }
+
+ before do
+ @runner1 = FactoryGirl.create(:ci_shared_runner, contacted_at: 1.year.ago)
+ @runner2 = FactoryGirl.create(:ci_shared_runner, contacted_at: 1.second.ago)
+ end
+
+ it { is_expected.to eq([@runner2])}
+ end
+
+ describe :online? do
+ let(:runner) { FactoryGirl.create(:ci_shared_runner) }
+
+ subject { runner.online? }
+
+ context 'never contacted' do
+ before { runner.contacted_at = nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'contacted long time ago time' do
+ before { runner.contacted_at = 1.year.ago }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'contacted 1s ago' do
+ before { runner.contacted_at = 1.second.ago }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe :status do
+ let(:runner) { FactoryGirl.create(:ci_shared_runner, contacted_at: 1.second.ago) }
+
+ subject { runner.status }
+
+ context 'never connected' do
+ before { runner.contacted_at = nil }
+
+ it { is_expected.to eq(:not_connected) }
+ end
+
+ context 'contacted 1s ago' do
+ before { runner.contacted_at = 1.second.ago }
+
+ it { is_expected.to eq(:online) }
+ end
+
+ context 'contacted long time ago' do
+ before { runner.contacted_at = 1.year.ago }
+
+ it { is_expected.to eq(:offline) }
+ end
+
+ context 'inactive' do
+ before { runner.active = false }
+
+ it { is_expected.to eq(:paused) }
+ end
+ end
+
describe "belongs_to_one_project?" do
it "returns false if there are two projects runner assigned to" do
runner = FactoryGirl.create(:ci_specific_runner)
diff --git a/spec/models/ci/service_spec.rb b/spec/models/ci/service_spec.rb
index 2c575056b08..36cda988eb4 100644
--- a/spec/models/ci/service_spec.rb
+++ b/spec/models/ci/service_spec.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: services
+# Table name: ci_services
#
# id :integer not null, primary key
# type :string(255)
@@ -29,13 +29,12 @@ describe Ci::Service do
end
describe "Testable" do
- let(:project) { FactoryGirl.create :ci_project }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:commit) { FactoryGirl.create :ci_commit }
let(:build) { FactoryGirl.create :ci_build, commit: commit }
before do
allow(@service).to receive_messages(
- project: project
+ project: commit.project
)
build
@testable = @service.can_test?
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 19c14ef2da2..b8aa3c1e777 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -1,3 +1,15 @@
+# == Schema Information
+#
+# Table name: ci_triggers
+#
+# id :integer not null, primary key
+# token :string(255)
+# project_id :integer not null
+# deleted_at :datetime
+# created_at :datetime
+# updated_at :datetime
+#
+
require 'spec_helper'
describe Ci::Trigger do
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index d034a6c7b9f..a515f5881ff 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: variables
+# Table name: ci_variables
#
# id :integer not null, primary key
# project_id :integer not null
diff --git a/spec/models/ci/web_hook_spec.rb b/spec/models/ci/web_hook_spec.rb
index bf9481ab81d..2865482a212 100644
--- a/spec/models/ci/web_hook_spec.rb
+++ b/spec/models/ci/web_hook_spec.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: web_hooks
+# Table name: ci_web_hooks
#
# id :integer not null, primary key
# url :string(255) not null
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index 1031af097bd..3c1009a2eb0 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -7,50 +7,72 @@ describe CommitRange do
it { is_expected.to include_module(Referable) }
end
- let(:sha_from) { 'f3f85602' }
- let(:sha_to) { 'e86e1013' }
+ let!(:project) { create(:project, :public) }
+ let!(:commit1) { project.commit("HEAD~2") }
+ let!(:commit2) { project.commit }
- let(:range) { described_class.new("#{sha_from}...#{sha_to}") }
- let(:range2) { described_class.new("#{sha_from}..#{sha_to}") }
+ let(:sha_from) { commit1.short_id }
+ let(:sha_to) { commit2.short_id }
+
+ let(:full_sha_from) { commit1.id }
+ let(:full_sha_to) { commit2.id }
+
+ let(:range) { described_class.new("#{sha_from}...#{sha_to}", project) }
+ let(:range2) { described_class.new("#{sha_from}..#{sha_to}", project) }
it 'raises ArgumentError when given an invalid range string' do
- expect { described_class.new("Foo") }.to raise_error(ArgumentError)
+ expect { described_class.new("Foo", project) }.to raise_error(ArgumentError)
end
describe '#to_s' do
it 'is correct for three-dot syntax' do
- expect(range.to_s).to eq "#{sha_from[0..7]}...#{sha_to[0..7]}"
+ expect(range.to_s).to eq "#{full_sha_from}...#{full_sha_to}"
end
it 'is correct for two-dot syntax' do
- expect(range2.to_s).to eq "#{sha_from[0..7]}..#{sha_to[0..7]}"
+ expect(range2.to_s).to eq "#{full_sha_from}..#{full_sha_to}"
end
end
describe '#to_reference' do
- let(:project) { double('project', to_reference: 'namespace1/project') }
+ let(:cross) { create(:project) }
+
+ it 'returns a String reference to the object' do
+ expect(range.to_reference).to eq "#{full_sha_from}...#{full_sha_to}"
+ end
+
+ it 'returns a String reference to the object' do
+ expect(range2.to_reference).to eq "#{full_sha_from}..#{full_sha_to}"
+ end
+
+ it 'supports a cross-project reference' do
+ expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{full_sha_from}...#{full_sha_to}"
+ end
+ end
- before do
- range.project = project
+ describe '#reference_link_text' do
+ let(:cross) { create(:project) }
+
+ it 'returns a String reference to the object' do
+ expect(range.reference_link_text).to eq "#{sha_from}...#{sha_to}"
end
it 'returns a String reference to the object' do
- expect(range.to_reference).to eq range.to_s
+ expect(range2.reference_link_text).to eq "#{sha_from}..#{sha_to}"
end
it 'supports a cross-project reference' do
- cross = double('project')
- expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{range.to_s}"
+ expect(range.reference_link_text(cross)).to eq "#{project.to_reference}@#{sha_from}...#{sha_to}"
end
end
describe '#reference_title' do
it 'returns the correct String for three-dot ranges' do
- expect(range.reference_title).to eq "Commits #{sha_from} through #{sha_to}"
+ expect(range.reference_title).to eq "Commits #{full_sha_from} through #{full_sha_to}"
end
it 'returns the correct String for two-dot ranges' do
- expect(range2.reference_title).to eq "Commits #{sha_from}^ through #{sha_to}"
+ expect(range2.reference_title).to eq "Commits #{full_sha_from}^ through #{full_sha_to}"
end
end
@@ -60,11 +82,11 @@ describe CommitRange do
end
it 'includes the correct values for a three-dot range' do
- expect(range.to_param).to eq({ from: sha_from, to: sha_to })
+ expect(range.to_param).to eq({ from: full_sha_from, to: full_sha_to })
end
it 'includes the correct values for a two-dot range' do
- expect(range2.to_param).to eq({ from: sha_from + '^', to: sha_to })
+ expect(range2.to_param).to eq({ from: full_sha_from + '^', to: full_sha_to })
end
end
@@ -79,64 +101,37 @@ describe CommitRange do
end
describe '#valid_commits?' do
- context 'without a project' do
- it 'returns nil' do
- expect(range.valid_commits?).to be_nil
+ context 'with a valid repo' do
+ before do
+ expect(project).to receive(:valid_repo?).and_return(true)
end
- end
-
- it 'accepts an optional project argument' do
- project1 = double('project1').as_null_object
- project2 = double('project2').as_null_object
-
- # project1 gets assigned through the accessor, but ignored when not given
- # as an argument to `valid_commits?`
- expect(project1).not_to receive(:present?)
- range.project = project1
-
- # project2 gets passed to `valid_commits?`
- expect(project2).to receive(:present?).and_return(false)
- range.valid_commits?(project2)
- end
-
- context 'with a project' do
- let(:project) { double('project', repository: double('repository')) }
+ it 'is false when `sha_from` is invalid' do
+ expect(project).to receive(:commit).with(sha_from).and_return(nil)
+ expect(project).to receive(:commit).with(sha_to).and_call_original
- context 'with a valid repo' do
- before do
- expect(project).to receive(:valid_repo?).and_return(true)
- range.project = project
- end
+ expect(range).not_to be_valid_commits
+ end
- it 'is false when `sha_from` is invalid' do
- expect(project.repository).to receive(:commit).with(sha_from).and_return(false)
- expect(project.repository).not_to receive(:commit).with(sha_to)
- expect(range).not_to be_valid_commits
- end
+ it 'is false when `sha_to` is invalid' do
+ expect(project).to receive(:commit).with(sha_from).and_call_original
+ expect(project).to receive(:commit).with(sha_to).and_return(nil)
- it 'is false when `sha_to` is invalid' do
- expect(project.repository).to receive(:commit).with(sha_from).and_return(true)
- expect(project.repository).to receive(:commit).with(sha_to).and_return(false)
- expect(range).not_to be_valid_commits
- end
+ expect(range).not_to be_valid_commits
+ end
- it 'is true when both `sha_from` and `sha_to` are valid' do
- expect(project.repository).to receive(:commit).with(sha_from).and_return(true)
- expect(project.repository).to receive(:commit).with(sha_to).and_return(true)
- expect(range).to be_valid_commits
- end
+ it 'is true when both `sha_from` and `sha_to` are valid' do
+ expect(range).to be_valid_commits
end
+ end
- context 'without a valid repo' do
- before do
- expect(project).to receive(:valid_repo?).and_return(false)
- range.project = project
- end
+ context 'without a valid repo' do
+ before do
+ expect(project).to receive(:valid_repo?).and_return(false)
+ end
- it 'returns false' do
- expect(range).not_to be_valid_commits
- end
+ it 'returns false' do
+ expect(range).not_to be_valid_commits
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index e303a97e6b5..38a3dc1f4a6 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -24,6 +24,17 @@ describe Commit do
end
end
+ describe '#reference_link_text' do
+ it 'returns a String reference to the object' do
+ expect(commit.reference_link_text).to eq commit.short_id
+ end
+
+ it 'supports a cross-project reference' do
+ cross = double('project')
+ expect(commit.reference_link_text(cross)).to eq "#{project.to_reference}@#{commit.short_id}"
+ end
+ end
+
describe '#title' do
it "returns no_commit_message when safe_message is blank" do
allow(commit).to receive(:safe_message).and_return('')
@@ -77,21 +88,17 @@ eos
let(:other_issue) { create :issue, project: other_project }
it 'detects issues that this commit is marked as closing' do
- allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid}")
- expect(commit.closes_issues).to eq([issue])
- end
-
- it 'does not detect issues from other projects' do
ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}"
- allow(commit).to receive(:safe_message).and_return("Fixes #{ext_ref}")
- expect(commit.closes_issues).to be_empty
+ allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid} and #{ext_ref}")
+ expect(commit.closes_issues).to include(issue)
+ expect(commit.closes_issues).to include(other_issue)
end
end
it_behaves_like 'a mentionable' do
- subject { commit }
+ subject { create(:project).commit }
- let(:author) { create(:user, email: commit.author_email) }
+ let(:author) { create(:user, email: subject.author_email) }
let(:backref_text) { "commit #{subject.id}" }
let(:set_mentionable_text) do
->(txt) { allow(subject).to receive(:safe_message).and_return(txt) }
@@ -100,4 +107,15 @@ eos
# Include the subject in the repository stub.
let(:extra_commits) { [subject] }
end
+
+ describe '#hook_attrs' do
+ let(:data) { commit.hook_attrs(with_changed_files: true) }
+
+ it { expect(data).to be_a(Hash) }
+ it { expect(data[:message]).to include('Add submodule from gitlab.com') }
+ it { expect(data[:timestamp]).to eq('2014-02-27T11:01:38+02:00') }
+ it { expect(data[:added]).to eq(["gitlab-grack"]) }
+ it { expect(data[:modified]).to eq([".gitmodules"]) }
+ it { expect(data[:removed]).to eq([]) }
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
new file mode 100644
index 00000000000..dca0715eed8
--- /dev/null
+++ b/spec/models/commit_status_spec.rb
@@ -0,0 +1,197 @@
+# == Schema Information
+#
+# Table name: ci_builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# coverage :float
+# commit_id :integer
+# commands :text
+# job_id :integer
+# name :string(255)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+# stage_idx :integer
+# tag :boolean
+# ref :string(255)
+# user_id :integer
+# type :string(255)
+# target_url :string(255)
+# description :string(255)
+# artifacts_file :text
+#
+
+require 'spec_helper'
+
+describe CommitStatus do
+ let(:commit) { FactoryGirl.create :ci_commit }
+ let(:commit_status) { FactoryGirl.create :commit_status, commit: commit }
+
+ it { is_expected.to belong_to(:commit) }
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) }
+
+ it { is_expected.to delegate_method(:sha).to(:commit) }
+ it { is_expected.to delegate_method(:short_sha).to(:commit) }
+ it { is_expected.to delegate_method(:gl_project).to(:commit) }
+
+ it { is_expected.to respond_to :success? }
+ it { is_expected.to respond_to :failed? }
+ it { is_expected.to respond_to :running? }
+ it { is_expected.to respond_to :pending? }
+
+ describe :author do
+ subject { commit_status.author }
+ before { commit_status.author = User.new }
+
+ it { is_expected.to eq(commit_status.user) }
+ end
+
+ describe :started? do
+ subject { commit_status.started? }
+
+ context 'without started_at' do
+ before { commit_status.started_at = nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ %w(running success failed).each do |status|
+ context "if commit status is #{status}" do
+ before { commit_status.status = status }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ %w(pending canceled).each do |status|
+ context "if commit status is #{status}" do
+ before { commit_status.status = status }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe :active? do
+ subject { commit_status.active? }
+
+ %w(pending running).each do |state|
+ context "if commit_status.status is #{state}" do
+ before { commit_status.status = state }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ %w(success failed canceled).each do |state|
+ context "if commit_status.status is #{state}" do
+ before { commit_status.status = state }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe :complete? do
+ subject { commit_status.complete? }
+
+ %w(success failed canceled).each do |state|
+ context "if commit_status.status is #{state}" do
+ before { commit_status.status = state }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ %w(pending running).each do |state|
+ context "if commit_status.status is #{state}" do
+ before { commit_status.status = state }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe :duration do
+ subject { commit_status.duration }
+
+ it { is_expected.to eq(120.0) }
+
+ context 'if the building process has not started yet' do
+ before do
+ commit_status.started_at = nil
+ commit_status.finished_at = nil
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'if the building process has started' do
+ before do
+ commit_status.started_at = Time.now - 1.minute
+ commit_status.finished_at = nil
+ end
+
+ it { is_expected.to be_a(Float) }
+ it { is_expected.to be > 0.0 }
+ end
+ end
+
+ describe :latest do
+ subject { CommitStatus.latest.order(:id) }
+
+ before do
+ @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running'
+ @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending'
+ @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'cc', status: 'success'
+ @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'bb', status: 'success'
+ @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'success'
+ end
+
+ it 'return unique statuses' do
+ is_expected.to eq([@commit2, @commit3, @commit4, @commit5])
+ end
+ end
+
+ describe :for_ref do
+ subject { CommitStatus.for_ref('bb').order(:id) }
+
+ before do
+ @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running'
+ @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending'
+ @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success'
+ end
+
+ it 'return statuses with equal and nil ref set' do
+ is_expected.to eq([@commit1])
+ end
+ end
+
+ describe :running_or_pending do
+ subject { CommitStatus.running_or_pending.order(:id) }
+
+ before do
+ @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running'
+ @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending'
+ @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success'
+ @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed'
+ @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled'
+ end
+
+ it 'return statuses that are running or pending' do
+ is_expected.to eq([@commit1, @commit2])
+ end
+ end
+end
diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb
new file mode 100644
index 00000000000..f7ed30f8198
--- /dev/null
+++ b/spec/models/concerns/case_sensitivity_spec.rb
@@ -0,0 +1,189 @@
+require 'spec_helper'
+
+describe CaseSensitivity do
+ describe '.iwhere' do
+ let(:connection) { ActiveRecord::Base.connection }
+ let(:model) { Class.new { include CaseSensitivity } }
+
+ describe 'using PostgreSQL' do
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ allow(Gitlab::Database).to receive(:mysql?).and_return(false)
+ end
+
+ describe 'with a single column/value pair' do
+ it 'returns the criteria for a column and a value' do
+ criteria = double(:criteria)
+
+ expect(connection).to receive(:quote_table_name).
+ with(:foo).
+ and_return('"foo"')
+
+ expect(model).to receive(:where).
+ with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar').
+ and_return(criteria)
+
+ expect(model.iwhere(foo: 'bar')).to eq(criteria)
+ end
+
+ it 'returns the criteria for a column with a table, and a value' do
+ criteria = double(:criteria)
+
+ expect(connection).to receive(:quote_table_name).
+ with(:'foo.bar').
+ and_return('"foo"."bar"')
+
+ expect(model).to receive(:where).
+ with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar').
+ and_return(criteria)
+
+ expect(model.iwhere(:'foo.bar' => 'bar')).to eq(criteria)
+ end
+ end
+
+ describe 'with multiple column/value pairs' do
+ it 'returns the criteria for a column and a value' do
+ initial = double(:criteria)
+ final = double(:criteria)
+
+ expect(connection).to receive(:quote_table_name).
+ with(:foo).
+ and_return('"foo"')
+
+ expect(connection).to receive(:quote_table_name).
+ with(:bar).
+ and_return('"bar"')
+
+ expect(model).to receive(:where).
+ with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar').
+ and_return(initial)
+
+ expect(initial).to receive(:where).
+ with(%q{LOWER("bar") = LOWER(:value)}, value: 'baz').
+ and_return(final)
+
+ got = model.iwhere(foo: 'bar', bar: 'baz')
+
+ expect(got).to eq(final)
+ end
+
+ it 'returns the criteria for a column with a table, and a value' do
+ initial = double(:criteria)
+ final = double(:criteria)
+
+ expect(connection).to receive(:quote_table_name).
+ with(:'foo.bar').
+ and_return('"foo"."bar"')
+
+ expect(connection).to receive(:quote_table_name).
+ with(:'foo.baz').
+ and_return('"foo"."baz"')
+
+ expect(model).to receive(:where).
+ with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar').
+ and_return(initial)
+
+ expect(initial).to receive(:where).
+ with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz').
+ and_return(final)
+
+ got = model.iwhere(:'foo.bar' => 'bar',
+ :'foo.baz' => 'baz')
+
+ expect(got).to eq(final)
+ end
+ end
+ end
+
+ describe 'using MySQL' do
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ allow(Gitlab::Database).to receive(:mysql?).and_return(true)
+ end
+
+ describe 'with a single column/value pair' do
+ it 'returns the criteria for a column and a value' do
+ criteria = double(:criteria)
+
+ expect(connection).to receive(:quote_table_name).
+ with(:foo).
+ and_return('`foo`')
+
+ expect(model).to receive(:where).
+ with(%q{`foo` = :value}, value: 'bar').
+ and_return(criteria)
+
+ expect(model.iwhere(foo: 'bar')).to eq(criteria)
+ end
+
+ it 'returns the criteria for a column with a table, and a value' do
+ criteria = double(:criteria)
+
+ expect(connection).to receive(:quote_table_name).
+ with(:'foo.bar').
+ and_return('`foo`.`bar`')
+
+ expect(model).to receive(:where).
+ with(%q{`foo`.`bar` = :value}, value: 'bar').
+ and_return(criteria)
+
+ expect(model.iwhere(:'foo.bar' => 'bar')).
+ to eq(criteria)
+ end
+ end
+
+ describe 'with multiple column/value pairs' do
+ it 'returns the criteria for a column and a value' do
+ initial = double(:criteria)
+ final = double(:criteria)
+
+ expect(connection).to receive(:quote_table_name).
+ with(:foo).
+ and_return('`foo`')
+
+ expect(connection).to receive(:quote_table_name).
+ with(:bar).
+ and_return('`bar`')
+
+ expect(model).to receive(:where).
+ with(%q{`foo` = :value}, value: 'bar').
+ and_return(initial)
+
+ expect(initial).to receive(:where).
+ with(%q{`bar` = :value}, value: 'baz').
+ and_return(final)
+
+ got = model.iwhere(foo: 'bar', bar: 'baz')
+
+ expect(got).to eq(final)
+ end
+
+ it 'returns the criteria for a column with a table, and a value' do
+ initial = double(:criteria)
+ final = double(:criteria)
+
+ expect(connection).to receive(:quote_table_name).
+ with(:'foo.bar').
+ and_return('`foo`.`bar`')
+
+ expect(connection).to receive(:quote_table_name).
+ with(:'foo.baz').
+ and_return('`foo`.`baz`')
+
+ expect(model).to receive(:where).
+ with(%q{`foo`.`bar` = :value}, value: 'bar').
+ and_return(initial)
+
+ expect(initial).to receive(:where).
+ with(%q{`foo`.`baz` = :value}, value: 'baz').
+ and_return(final)
+
+ got = model.iwhere(:'foo.bar' => 'bar',
+ :'foo.baz' => 'baz')
+
+ expect(got).to eq(final)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 8f706f8934b..0f13c4410cd 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -68,7 +68,6 @@ describe Issue, "Issuable" do
end
end
-
describe "#to_hook_data" do
let(:hook_data) { issue.to_hook_data(user) }
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 2d6fe003215..6179882e935 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -25,7 +25,7 @@ describe Issue, "Mentionable" do
it 'correctly removes already-mentioned Commits' do
expect(SystemNoteService).not_to receive(:cross_reference)
- issue.create_cross_references!(project, author, [commit2])
+ issue.create_cross_references!(author, [commit2])
end
end
diff --git a/spec/models/concerns/strip_attribute_spec.rb b/spec/models/concerns/strip_attribute_spec.rb
new file mode 100644
index 00000000000..6445e29c3ef
--- /dev/null
+++ b/spec/models/concerns/strip_attribute_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Milestone, "StripAttribute" do
+ let(:milestone) { create(:milestone) }
+
+ describe ".strip_attributes" do
+ it { expect(Milestone).to respond_to(:strip_attributes) }
+ it { expect(Milestone.strip_attrs).to include(:title) }
+ end
+
+ describe "#strip_attributes" do
+ before do
+ milestone.title = ' 8.3 '
+ milestone.valid?
+ end
+
+ it { expect(milestone.title).to eq('8.3') }
+ end
+
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 0f32f162a10..ae53f7a536b 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -64,4 +64,42 @@ describe Event do
it { expect(@event.branch_name).to eq("master") }
it { expect(@event.author).to eq(@user) }
end
+
+ describe '.latest_update_time' do
+ describe 'when events are present' do
+ let(:time) { Time.utc(2015, 1, 1) }
+
+ before do
+ create(:closed_issue_event, updated_at: time)
+ create(:closed_issue_event, updated_at: time + 5)
+ end
+
+ it 'returns the latest update time' do
+ expect(Event.latest_update_time).to eq(time + 5)
+ end
+ end
+
+ describe 'when no events exist' do
+ it 'returns nil' do
+ expect(Event.latest_update_time).to be_nil
+ end
+ end
+ end
+
+ describe '.limit_recent' do
+ let!(:event1) { create(:closed_issue_event) }
+ let!(:event2) { create(:closed_issue_event) }
+
+ describe 'without an explicit limit' do
+ subject { Event.limit_recent }
+
+ it { is_expected.to eq([event2, event1]) }
+ end
+
+ describe 'with an explicit limit' do
+ subject { Event.limit_recent(1) }
+
+ it { is_expected.to eq([event2]) }
+ end
+ end
end
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
new file mode 100644
index 00000000000..c86314c454c
--- /dev/null
+++ b/spec/models/generic_commit_status_spec.rb
@@ -0,0 +1,72 @@
+# == Schema Information
+#
+# Table name: ci_builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# coverage :float
+# commit_id :integer
+# commands :text
+# job_id :integer
+# name :string(255)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+# stage_idx :integer
+# tag :boolean
+# ref :string(255)
+# user_id :integer
+# type :string(255)
+# target_url :string(255)
+# description :string(255)
+# artifacts_file :text
+#
+
+require 'spec_helper'
+
+describe GenericCommitStatus do
+ let(:commit) { FactoryGirl.create :ci_commit }
+ let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, commit: commit }
+
+ describe :context do
+ subject { generic_commit_status.context }
+ before { generic_commit_status.context = 'my_context' }
+
+ it { is_expected.to eq(generic_commit_status.name) }
+ end
+
+ describe :tags do
+ subject { generic_commit_status.tags }
+
+ it { is_expected.to eq([:external]) }
+ end
+
+ describe :set_default_values do
+ before do
+ generic_commit_status.context = nil
+ generic_commit_status.stage = nil
+ generic_commit_status.save
+ end
+
+ describe :context do
+ subject { generic_commit_status.context }
+
+ it { is_expected.to_not be_nil }
+ end
+
+ describe :stage do
+ subject { generic_commit_status.stage }
+
+ it { is_expected.to_not be_nil }
+ end
+ end
+end
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
new file mode 100644
index 00000000000..6eeff30b20e
--- /dev/null
+++ b/spec/models/global_milestone_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe GlobalMilestone do
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project1) { create(:project, group: group) }
+ let(:project2) { create(:project, path: 'gitlab-ci', group: group) }
+ let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) }
+ let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
+ let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
+ let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) }
+ let(:milestone2_project1) { create(:milestone, title: "VD-123", project: project1) }
+ let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) }
+ let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) }
+
+ describe :build_collection do
+ before do
+ milestones =
+ [
+ milestone1_project1,
+ milestone1_project2,
+ milestone1_project3,
+ milestone2_project1,
+ milestone2_project2,
+ milestone2_project3
+ ]
+
+ @global_milestones = GlobalMilestone.build_collection(milestones)
+ end
+
+ it 'should have all project milestones' do
+ expect(@global_milestones.count).to eq(2)
+ end
+
+ it 'should have all project milestones titles' do
+ expect(@global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'VD-123'])
+ end
+
+ it 'should have all project milestones' do
+ expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
+ end
+ end
+
+ describe :initialize do
+ before do
+ milestones =
+ [
+ milestone1_project1,
+ milestone1_project2,
+ milestone1_project3,
+ ]
+
+ @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones)
+ end
+
+ it 'should have exactly one group milestone' do
+ expect(@global_milestone.title).to eq('Milestone v1.2')
+ end
+
+ it 'should have all project milestones with the same title' do
+ expect(@global_milestone.milestones.count).to eq(3)
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 80638fc8db2..6f166b5ab75 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -11,6 +11,7 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
+# public :boolean default(FALSE)
#
require 'spec_helper'
@@ -37,6 +38,33 @@ describe Group do
it { is_expected.not_to validate_presence_of :owner }
end
+ describe '.public_and_given_groups' do
+ let!(:public_group) { create(:group, public: true) }
+
+ subject { described_class.public_and_given_groups([group.id]) }
+
+ it { is_expected.to eq([public_group, group]) }
+ end
+
+ describe '.visible_to_user' do
+ let!(:group) { create(:group) }
+ let!(:user) { create(:user) }
+
+ subject { described_class.visible_to_user(user) }
+
+ describe 'when the user has access to a group' do
+ before do
+ group.add_user(user, Gitlab::Access::MASTER)
+ end
+
+ it { is_expected.to eq([group]) }
+ end
+
+ describe 'when the user does not have access to any groups' do
+ it { is_expected.to eq([]) }
+ end
+ end
+
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(group.to_reference).to eq "@#{group.name}"
@@ -84,4 +112,23 @@ describe Group do
expect(group.avatar_type).to eq(["only images allowed"])
end
end
+
+ describe "public_profile?" do
+ it "returns true for public group" do
+ group = create(:group, public: true)
+ expect(group.public_profile?).to be_truthy
+ end
+
+ it "returns true for non-public group with public project" do
+ group = create(:group)
+ create(:project, :public, group: group)
+ expect(group.public_profile?).to be_truthy
+ end
+
+ it "returns false for non-public group with no public projects" do
+ group = create(:group)
+ create(:project, group: group)
+ expect(group.public_profile?).to be_falsy
+ end
+ end
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index dae7e399cfb..a2dc66fce3e 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -22,7 +22,7 @@ describe ProjectHook do
describe '.push_hooks' do
it 'should return hooks for push events only' do
hook = create(:project_hook, push_events: true)
- hook2 = create(:project_hook, push_events: false)
+ create(:project_hook, push_events: false)
expect(ProjectHook.push_hooks).to eq([hook])
end
end
@@ -30,7 +30,7 @@ describe ProjectHook do
describe '.tag_push_hooks' do
it 'should return hooks for tag push events only' do
hook = create(:project_hook, tag_push_events: true)
- hook2 = create(:project_hook, tag_push_events: false)
+ create(:project_hook, tag_push_events: false)
expect(ProjectHook.tag_push_hooks).to eq([hook])
end
end
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 4c8b8910ae7..16641c12124 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -39,8 +39,6 @@ describe ServiceHook do
end
it "POSTs the data as JSON" do
- json = @data.to_json
-
@service_hook.execute(@data)
expect(WebMock).to have_requested(:post, @service_hook.url).with(
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook' }
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 23f30881d99..35042788c65 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -60,8 +60,6 @@ describe ProjectHook do
end
it "POSTs the data as JSON" do
- json = @data.to_json
-
@project_hook.execute(@data, 'push_hooks')
expect(WebMock).to have_requested(:post, @project_hook.url).with(
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook' }
@@ -73,5 +71,11 @@ describe ProjectHook do
expect { @project_hook.execute(@data, 'push_hooks') }.to raise_error(RuntimeError)
end
+
+ it "handles SSL exceptions" do
+ expect(WebHook).to receive(:post).and_raise(OpenSSL::SSL::SSLError.new('SSL error'))
+
+ expect(@project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error'])
+ end
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index cf336d82957..c9aa1b063c6 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -68,8 +68,45 @@ describe Issue do
end
end
+ describe '#closed_by_merge_requests' do
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project, state: "opened")}
+ let(:closed_issue) { build(:issue, project: project, state: "closed")}
+
+ let(:mr) do
+ opts = {
+ title: 'Awesome merge_request',
+ description: "Fixes #{issue.to_reference}",
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ MergeRequests::CreateService.new(project, project.owner, opts).execute
+ end
+
+ let(:closed_mr) do
+ opts = {
+ title: 'Awesome merge_request 2',
+ description: "Fixes #{issue.to_reference}",
+ source_branch: 'feature',
+ target_branch: 'master',
+ state: 'closed'
+ }
+ MergeRequests::CreateService.new(project, project.owner, opts).execute
+ end
+
+ it 'returns the merge request to close this issue' do
+ allow(mr).to receive(:closes_issue?).with(issue).and_return(true)
+
+ expect(issue.closed_by_merge_requests).to eq([mr])
+ end
+
+ it "returns an empty array when the current issue is closed already" do
+ expect(closed_issue.closed_by_merge_requests).to eq([])
+ end
+ end
+
it_behaves_like 'an editable mentionable' do
- subject { create(:issue, project: project) }
+ subject { create(:issue) }
let(:backref_text) { "issue #{subject.to_reference}" }
let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 6518213d71c..511ee8cbd96 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -8,6 +8,7 @@
# project_id :integer
# created_at :datetime
# updated_at :datetime
+# template :boolean default(FALSE)
#
require 'spec_helper'
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 17a49013d25..567c911425c 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -20,6 +20,7 @@
# position :integer default(0)
# locked_at :datetime
# updated_by_id :integer
+# merge_error :string(255)
#
require 'spec_helper'
@@ -79,6 +80,12 @@ describe MergeRequest do
expect(merge_request.commits).not_to be_empty
expect(merge_request.mr_and_commit_notes.count).to eq(2)
end
+
+ it "should include notes for commits from target project as well" do
+ create(:note, commit_id: merge_request.commits.first.id, noteable_type: 'Commit', project: merge_request.target_project)
+ expect(merge_request.commits).not_to be_empty
+ expect(merge_request.mr_and_commit_notes.count).to eq(3)
+ end
end
describe '#is_being_reassigned?' do
@@ -165,6 +172,17 @@ describe MergeRequest do
end
end
+ describe "#hook_attrs" do
+ it "has all the required keys" do
+ attrs = subject.hook_attrs
+ attrs = attrs.to_h
+ expect(attrs).to include(:source)
+ expect(attrs).to include(:target)
+ expect(attrs).to include(:last_commit)
+ expect(attrs).to include(:work_in_progress)
+ end
+ end
+
it_behaves_like 'an editable mentionable' do
subject { create(:merge_request) }
@@ -175,4 +193,29 @@ describe MergeRequest do
it_behaves_like 'a Taskable' do
subject { create :merge_request, :simple }
end
+
+ describe '#ci_commit' do
+ describe 'when the source project exists' do
+ it 'returns the latest commit' do
+ commit = double(:commit, id: '123abc')
+ ci_commit = double(:ci_commit)
+
+ allow(subject).to receive(:last_commit).and_return(commit)
+
+ expect(subject.source_project).to receive(:ci_commit).
+ with('123abc').
+ and_return(ci_commit)
+
+ expect(subject.ci_commit).to eq(ci_commit)
+ end
+ end
+
+ describe 'when the source project does not exist' do
+ it 'returns nil' do
+ allow(subject).to receive(:source_project).and_return(nil)
+
+ expect(subject.ci_commit).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 36352e1ecce..77c58627322 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -111,8 +111,8 @@ describe Milestone do
describe :is_empty? do
before do
- issue = create :closed_issue, milestone: milestone
- merge_request = create :merge_request, milestone: milestone
+ create :closed_issue, milestone: milestone
+ create :merge_request, milestone: milestone
end
it 'Should return total count of issues and merge requests assigned to milestone' do
@@ -125,7 +125,7 @@ describe Milestone do
milestone = create :milestone
create :closed_issue, milestone: milestone
- issue = create :issue
+ create :issue
end
it 'should be true if milestone active and all nested issues closed' do
@@ -140,4 +140,32 @@ describe Milestone do
end
end
+ describe '#sort_issues' do
+ let(:milestone) { create(:milestone) }
+
+ let(:issue1) { create(:issue, milestone: milestone, position: 1) }
+ let(:issue2) { create(:issue, milestone: milestone, position: 2) }
+ let(:issue3) { create(:issue, milestone: milestone, position: 3) }
+ let(:issue4) { create(:issue, position: 42) }
+
+ it 'sorts the given issues' do
+ milestone.sort_issues([issue3.id, issue2.id, issue1.id])
+
+ issue1.reload
+ issue2.reload
+ issue3.reload
+
+ expect(issue1.position).to eq(3)
+ expect(issue2.position).to eq(2)
+ expect(issue3.position).to eq(1)
+ end
+
+ it 'ignores issues not part of the milestone' do
+ milestone.sort_issues([issue3.id, issue2.id, issue1.id, issue4.id])
+
+ issue4.reload
+
+ expect(issue4.position).to eq(42)
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 1d72a9503ae..a98b9cb7321 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -11,6 +11,7 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
+# public :boolean default(FALSE)
#
require 'spec_helper'
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 3a0b194ba1e..f347f537550 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -32,77 +32,6 @@ describe Note do
it { is_expected.to validate_presence_of(:project) }
end
- describe '#votable?' do
- it 'is true for issue notes' do
- note = build(:note_on_issue)
- expect(note).to be_votable
- end
-
- it 'is true for merge request notes' do
- note = build(:note_on_merge_request)
- expect(note).to be_votable
- end
-
- it 'is false for merge request diff notes' do
- note = build(:note_on_merge_request_diff)
- expect(note).not_to be_votable
- end
-
- it 'is false for commit notes' do
- note = build(:note_on_commit)
- expect(note).not_to be_votable
- end
-
- it 'is false for commit diff notes' do
- note = build(:note_on_commit_diff)
- expect(note).not_to be_votable
- end
- end
-
- describe 'voting score' do
- it 'recognizes a neutral note' do
- note = build(:votable_note, note: 'This is not a +1 note')
- expect(note).not_to be_upvote
- expect(note).not_to be_downvote
- end
-
- it 'recognizes a neutral emoji note' do
- note = build(:votable_note, note: "I would :+1: this, but I don't want to")
- expect(note).not_to be_upvote
- expect(note).not_to be_downvote
- end
-
- it 'recognizes a +1 note' do
- note = build(:votable_note, note: '+1 for this')
- expect(note).to be_upvote
- end
-
- it 'recognizes a +1 emoji as a vote' do
- note = build(:votable_note, note: ':+1: for this')
- expect(note).to be_upvote
- end
-
- it 'recognizes a thumbsup emoji as a vote' do
- note = build(:votable_note, note: ':thumbsup: for this')
- expect(note).to be_upvote
- end
-
- it 'recognizes a -1 note' do
- note = build(:votable_note, note: '-1 for this')
- expect(note).to be_downvote
- end
-
- it 'recognizes a -1 emoji as a vote' do
- note = build(:votable_note, note: ':-1: for this')
- expect(note).to be_downvote
- end
-
- it 'recognizes a thumbsdown emoji as a vote' do
- note = build(:votable_note, note: ':thumbsdown: for this')
- expect(note).to be_downvote
- end
- end
-
describe "Commit notes" do
let!(:note) { create(:note_on_commit, note: "+1 from me") }
let!(:commit) { note.noteable }
@@ -139,10 +68,6 @@ describe Note do
it "should be recognized by #for_commit_diff_line?" do
expect(note).to be_for_commit_diff_line
end
-
- it "should not be votable" do
- expect(note).not_to be_votable
- end
end
describe 'authorization' do
@@ -192,10 +117,9 @@ describe Note do
end
it_behaves_like 'an editable mentionable' do
- subject { create :note, noteable: issue, project: project }
+ subject { create :note, noteable: issue, project: issue.project }
- let(:project) { create(:project) }
- let(:issue) { create :issue, project: project }
+ let(:issue) { create :issue }
let(:backref_text) { issue.gfm_reference }
let(:set_mentionable_text) { ->(txt) { subject.note = txt } }
end
@@ -205,4 +129,16 @@ describe Note do
it { expect(Note.search('wow')).to include(note) }
end
+
+ describe :grouped_awards do
+ before do
+ create :note, note: "smile", is_award: true
+ create :note, note: "smile", is_award: true
+ end
+
+ it "returns grouped array of notes" do
+ expect(Note.grouped_awards.first.first).to eq("smile")
+ expect(Note.grouped_awards.first.last).to match_array(Note.all)
+ end
+ end
end
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
new file mode 100644
index 00000000000..c34b2487ecf
--- /dev/null
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -0,0 +1,94 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+# push_events :boolean default(TRUE)
+# issues_events :boolean default(TRUE)
+# merge_requests_events :boolean default(TRUE)
+# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
+#
+
+require 'spec_helper'
+
+describe BambooService, models: true do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe "Execute" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ context "when a password was previously set" do
+ before do
+ @bamboo_service = BambooService.create(
+ project: create(:project),
+ properties: {
+ bamboo_url: 'http://gitlab.com',
+ username: 'mic',
+ password: "password"
+ }
+ )
+ end
+
+ it "reset password if url changed" do
+ @bamboo_service.bamboo_url = 'http://gitlab1.com'
+ @bamboo_service.save
+ expect(@bamboo_service.password).to be_nil
+ end
+
+ it "does not reset password if username changed" do
+ @bamboo_service.username = "some_name"
+ @bamboo_service.save
+ expect(@bamboo_service.password).to eq("password")
+ end
+
+ it "does not reset password if new url is set together with password, even if it's the same password" do
+ @bamboo_service.bamboo_url = 'http://gitlab_edited.com'
+ @bamboo_service.password = 'password'
+ @bamboo_service.save
+ expect(@bamboo_service.password).to eq("password")
+ expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com")
+ end
+
+ it "should reset password if url changed, even if setter called multiple times" do
+ @bamboo_service.bamboo_url = 'http://gitlab1.com'
+ @bamboo_service.bamboo_url = 'http://gitlab1.com'
+ @bamboo_service.save
+ expect(@bamboo_service.password).to be_nil
+ end
+ end
+
+ context "when no password was previously set" do
+ before do
+ @bamboo_service = BambooService.create(
+ project: create(:project),
+ properties: {
+ bamboo_url: 'http://gitlab.com',
+ username: 'mic'
+ }
+ )
+ end
+
+ it "saves password if new url is set together with password" do
+ @bamboo_service.bamboo_url = 'http://gitlab_edited.com'
+ @bamboo_service.password = 'password'
+ @bamboo_service.save
+ expect(@bamboo_service.password).to eq("password")
+ expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com")
+ end
+
+ end
+ end
+end
diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb
index 8cdd551a0ca..b9006b693b2 100644
--- a/spec/models/project_services/gitlab_ci_service_spec.rb
+++ b/spec/models/project_services/gitlab_ci_service_spec.rb
@@ -39,8 +39,7 @@ describe GitlabCiService do
end
describe :build_page do
- it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://localhost/ci/projects/#{@ci_project.id}/refs/master/commits/2ab7834c")}
- it { expect(@service.build_page("issue#2", 'master')).to eq("http://localhost/ci/projects/#{@ci_project.id}/refs/master/commits/issue%232")}
+ it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://localhost/#{@ci_project.gl_project.path_with_namespace}/commit/2ab7834c/builds")}
end
describe "execute" do
@@ -48,33 +47,11 @@ describe GitlabCiService do
let(:project) { create(:project, name: 'project') }
let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
- it "calls ci_yaml_file" do
- service_hook = double
- expect(@service).to receive(:ci_yaml_file).with(push_sample_data[:checkout_sha])
+ it "calls CreateCommitService" do
+ expect_any_instance_of(Ci::CreateCommitService).to receive(:execute).with(@ci_project, user, push_sample_data)
@service.execute(push_sample_data)
end
end
end
-
- describe "Fork registration" do
- before do
- @old_project = create(:ci_project).gl_project
- @project = create(:empty_project)
- @user = create(:user)
-
- @service = GitlabCiService.new
- allow(@service).to receive_messages(
- service_hook: true,
- project_url: 'http://ci.gitlab.org/projects/2',
- token: 'verySecret',
- project: @old_project
- )
- end
-
- it "creates fork on CI" do
- expect_any_instance_of(Ci::CreateProjectService).to receive(:execute)
- @service.fork_registration(@project, @user)
- end
- end
end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 65d16beef91..f67d7b30980 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -87,7 +87,7 @@ describe HipchatService do
it "should create a push message" do
message = hipchat.send(:create_push_message, push_sample_data)
- obj_attr = push_sample_data[:object_attributes]
+ push_sample_data[:object_attributes]
branch = push_sample_data[:ref].gsub('refs/heads/', '')
expect(message).to include("#{user.name} pushed to branch " \
"<a href=\"#{project.web_url}/commits/#{branch}\">#{branch}</a> of " \
@@ -107,7 +107,7 @@ describe HipchatService do
it "should create a tag push message" do
message = hipchat.send(:create_push_message, push_sample_data)
- obj_attr = push_sample_data[:object_attributes]
+ push_sample_data[:object_attributes]
expect(message).to eq("#{user.name} pushed new tag " \
"<a href=\"#{project.web_url}/commits/test\">test</a> to " \
"<a href=\"#{project.web_url}\">#{project_name}</a>\n")
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index ddd2cce212c..576f5fc79eb 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -94,9 +94,9 @@ describe JiraService do
end
it 'should be prepopulated with the settings' do
- expect(@service.properties[:project_url]).to eq('http://jira.sample/projects/project_a')
- expect(@service.properties[:issues_url]).to eq("http://jira.sample/issues/:id")
- expect(@service.properties[:new_issue_url]).to eq("http://jira.sample/projects/project_a/issues/new")
+ expect(@service.properties["project_url"]).to eq('http://jira.sample/projects/project_a')
+ expect(@service.properties["issues_url"]).to eq("http://jira.sample/issues/:id")
+ expect(@service.properties["new_issue_url"]).to eq("http://jira.sample/projects/project_a/issues/new")
end
end
end
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
new file mode 100644
index 00000000000..f26b47a856c
--- /dev/null
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -0,0 +1,93 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+# push_events :boolean default(TRUE)
+# issues_events :boolean default(TRUE)
+# merge_requests_events :boolean default(TRUE)
+# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
+#
+
+require 'spec_helper'
+
+describe TeamcityService, models: true do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe "Execute" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ context "when a password was previously set" do
+ before do
+ @teamcity_service = TeamcityService.create(
+ project: create(:project),
+ properties: {
+ teamcity_url: 'http://gitlab.com',
+ username: 'mic',
+ password: "password"
+ }
+ )
+ end
+
+ it "reset password if url changed" do
+ @teamcity_service.teamcity_url = 'http://gitlab1.com'
+ @teamcity_service.save
+ expect(@teamcity_service.password).to be_nil
+ end
+
+ it "does not reset password if username changed" do
+ @teamcity_service.username = "some_name"
+ @teamcity_service.save
+ expect(@teamcity_service.password).to eq("password")
+ end
+
+ it "does not reset password if new url is set together with password, even if it's the same password" do
+ @teamcity_service.teamcity_url = 'http://gitlab_edited.com'
+ @teamcity_service.password = 'password'
+ @teamcity_service.save
+ expect(@teamcity_service.password).to eq("password")
+ expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com")
+ end
+
+ it "should reset password if url changed, even if setter called multiple times" do
+ @teamcity_service.teamcity_url = 'http://gitlab1.com'
+ @teamcity_service.teamcity_url = 'http://gitlab1.com'
+ @teamcity_service.save
+ expect(@teamcity_service.password).to be_nil
+ end
+ end
+
+ context "when no password was previously set" do
+ before do
+ @teamcity_service = TeamcityService.create(
+ project: create(:project),
+ properties: {
+ teamcity_url: 'http://gitlab.com',
+ username: 'mic'
+ }
+ )
+ end
+
+ it "saves password if new url is set together with password" do
+ @teamcity_service.teamcity_url = 'http://gitlab_edited.com'
+ @teamcity_service.password = 'password'
+ @teamcity_service.save
+ expect(@teamcity_service.password).to eq("password")
+ expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com")
+ end
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 9e7b6f5cb30..06a02c13bf1 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -140,7 +140,7 @@ describe Project do
describe 'last_activity_date' do
it 'returns the creation date of the project\'s last event if present' do
- last_activity_event = create(:event, project: project)
+ create(:event, project: project)
expect(project.last_activity_at.to_i).to eq(last_event.created_at.to_i)
end
@@ -220,6 +220,7 @@ describe Project do
end
it { expect(Project.find_with_namespace('gitlab/gitlabhq')).to eq(@project) }
+ it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) }
it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil }
end
end
@@ -344,17 +345,6 @@ describe Project do
expect(project1.star_count).to eq(0)
expect(project2.star_count).to eq(0)
end
-
- it 'is decremented when an upvoter account is deleted' do
- user = create :user
- project = create :project, :public
- user.toggle_star(project)
- project.reload
- expect(project.star_count).to eq(1)
- user.destroy
- project.reload
- expect(project.star_count).to eq(0)
- end
end
describe :avatar_type do
@@ -404,21 +394,84 @@ describe Project do
describe :ci_commit do
let(:project) { create :project }
- let(:ci_project) { create :ci_project, gl_project: project }
- let(:commit) { create :ci_commit, project: ci_project }
+ let(:commit) { create :ci_commit, gl_project: project }
- before { project.create_gitlab_ci_service(active: true) }
+ before do
+ project.ensure_gitlab_ci_project
+ project.create_gitlab_ci_service(active: true)
+ end
it { expect(project.ci_commit(commit.sha)).to eq(commit) }
end
- describe :enable_ci do
+ describe :builds_enabled do
let(:project) { create :project }
- let(:user) { create :user }
- before { project.enable_ci(user) }
+ before { project.builds_enabled = true }
+
+ subject { project.builds_enabled }
- it { expect(project.gitlab_ci?).to be_truthy }
+ it { is_expected.to eq(project.gitlab_ci_service.active) }
+ it { expect(project.builds_enabled?).to be_truthy }
it { expect(project.gitlab_ci_project).to be_a(Ci::Project) }
end
+
+ describe '.trending' do
+ let(:group) { create(:group) }
+ let(:project1) { create(:empty_project, :public, group: group) }
+ let(:project2) { create(:empty_project, :public, group: group) }
+
+ before do
+ 2.times do
+ create(:note_on_commit, project: project1)
+ end
+
+ create(:note_on_commit, project: project2)
+ end
+
+ describe 'without an explicit start date' do
+ subject { described_class.trending.to_a }
+
+ it 'sorts Projects by the amount of notes in descending order' do
+ expect(subject).to eq([project1, project2])
+ end
+ end
+
+ describe 'with an explicit start date' do
+ let(:date) { 2.months.ago }
+
+ subject { described_class.trending(date).to_a }
+
+ before do
+ 2.times do
+ # Little fix for special issue related to Fractional Seconds support for MySQL.
+ # See: https://github.com/rails/rails/pull/14359/files
+ create(:note_on_commit, project: project2, created_at: date + 1)
+ end
+ end
+
+ it 'sorts Projects by the amount of notes in descending order' do
+ expect(subject).to eq([project2, project1])
+ end
+ end
+ end
+
+ describe '.visible_to_user' do
+ let!(:project) { create(:project, :private) }
+ let!(:user) { create(:user) }
+
+ subject { described_class.visible_to_user(user) }
+
+ describe 'when a user has access to a project' do
+ before do
+ project.team.add_user(user, Gitlab::Access::MASTER)
+ end
+
+ it { is_expected.to eq([project]) }
+ end
+
+ describe 'when a user does not have access to any projects' do
+ it { is_expected.to eq([]) }
+ end
+ end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index cc1138490a0..26e8fdae472 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -66,4 +66,16 @@ describe ProjectTeam do
it { expect(project.team.member?(guest)).to be_truthy }
end
end
+
+ describe "#human_max_access" do
+ it "return master role" do
+ user = create :user
+ group = create :group
+ group.add_users([user.id], GroupMember::MASTER)
+ project = create(:project, namespace: group)
+ project.team << [user, :guest]
+
+ expect(project.team.human_max_access(user.id)).to eq("Master")
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index f785203af7d..3b889144447 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -184,6 +184,12 @@ describe ProjectWiki do
subject.create_page("test page", "some content", :markdown, "commit message")
expect(subject.pages.first.page.version.message).to eq("commit message")
end
+
+ it 'updates project activity' do
+ expect(subject).to receive(:update_project_activity)
+
+ subject.create_page('Test Page', 'This is content')
+ end
end
describe "#update_page" do
@@ -205,6 +211,12 @@ describe ProjectWiki do
it "sets the correct commit message" do
expect(@page.version.message).to eq("updated page")
end
+
+ it 'updates project activity' do
+ expect(subject).to receive(:update_project_activity)
+
+ subject.update_page(@gollum_page, 'Yet more content', :markdown, 'Updated page again')
+ end
end
describe "#delete_page" do
@@ -217,13 +229,19 @@ describe ProjectWiki do
subject.delete_page(@page)
expect(subject.pages.count).to eq(0)
end
+
+ it 'updates project activity' do
+ expect(subject).to receive(:update_project_activity)
+
+ subject.delete_page(@page)
+ end
end
private
def create_temp_repo(path)
FileUtils.mkdir_p path
- system(*%W(git init --quiet --bare -- #{path}))
+ system(*%W(#{Gitlab.config.git.bin_path} init --quiet --bare -- #{path}))
end
def remove_temp_repo(path)
@@ -231,7 +249,7 @@ describe ProjectWiki do
end
def commit_details
- commit = { name: user.name, email: user.email, message: "test commit" }
+ { name: user.name, email: user.email, message: "test commit" }
end
def create_page(name, content)
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
new file mode 100644
index 00000000000..72ecb442a36
--- /dev/null
+++ b/spec/models/release_spec.rb
@@ -0,0 +1,28 @@
+# == Schema Information
+#
+# Table name: releases
+#
+# id :integer not null, primary key
+# tag :string(255)
+# description :text
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+#
+
+require 'rails_helper'
+
+RSpec.describe Release, type: :model do
+ let(:release) { create(:release) }
+
+ it { expect(release).to be_valid }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validation' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:description) }
+ end
+end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 05e51532eb8..fa261e64c35 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -4,6 +4,7 @@ describe Repository do
include RepoHelpers
let(:repository) { create(:project).repository }
+ let(:user) { create(:user) }
describe :branch_names_contains do
subject { repository.branch_names_contains(sample_commit.id) }
@@ -26,6 +27,15 @@ describe Repository do
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
end
+ describe :find_commits_by_message do
+ subject { repository.find_commits_by_message('submodule').map{ |k| k.id } }
+
+ it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ it { is_expected.to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ it { is_expected.to include('cfe32cf61b73a0d5e9f13e774abde7ff789b1660') }
+ it { is_expected.not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+ end
+
describe :blob_at do
context 'blank sha' do
subject { repository.blob_at(Gitlab::Git::BLANK_SHA, '.gitignore') }
@@ -90,5 +100,104 @@ describe Repository do
it { expect(subject.startline).to eq(186) }
it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") }
end
+
+ end
+
+ describe :add_branch do
+ context 'when pre hooks were successful' do
+ it 'should run without errors' do
+ hook = double(trigger: true)
+ expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
+
+ expect { repository.add_branch(user, 'new_feature', 'master') }.not_to raise_error
+ end
+
+ it 'should create the branch' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
+
+ branch = repository.add_branch(user, 'new_feature', 'master')
+
+ expect(branch.name).to eq('new_feature')
+ end
+ end
+
+ context 'when pre hooks failed' do
+ it 'should get an error' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
+
+ expect do
+ repository.add_branch(user, 'new_feature', 'master')
+ end.to raise_error(GitHooksService::PreReceiveError)
+ end
+
+ it 'should not create the branch' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
+
+ expect do
+ repository.add_branch(user, 'new_feature', 'master')
+ end.to raise_error(GitHooksService::PreReceiveError)
+ expect(repository.find_branch('new_feature')).to be_nil
+ end
+ end
+ end
+
+ describe :rm_branch do
+ context 'when pre hooks were successful' do
+ it 'should run without errors' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
+
+ expect { repository.rm_branch(user, 'feature') }.not_to raise_error
+ end
+
+ it 'should delete the branch' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
+
+ expect { repository.rm_branch(user, 'feature') }.not_to raise_error
+
+ expect(repository.find_branch('feature')).to be_nil
+ end
+ end
+
+ context 'when pre hooks failed' do
+ it 'should get an error' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
+
+ expect do
+ repository.rm_branch(user, 'new_feature')
+ end.to raise_error(GitHooksService::PreReceiveError)
+ end
+
+ it 'should not delete the branch' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
+
+ expect do
+ repository.rm_branch(user, 'feature')
+ end.to raise_error(GitHooksService::PreReceiveError)
+ expect(repository.find_branch('feature')).not_to be_nil
+ end
+ end
end
+
+ describe :commit_with_hooks do
+ context 'when pre hooks were successful' do
+ it 'should run without errors' do
+ expect_any_instance_of(GitHooksService).to receive(:execute).and_return(true)
+
+ expect do
+ repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+ end.not_to raise_error
+ end
+ end
+
+ context 'when pre hooks failed' do
+ it 'should get an error' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
+
+ expect do
+ repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+ end.to raise_error(GitHooksService::PreReceiveError)
+ end
+ end
+ end
+
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index a213ffe6c4b..692e5fda3ba 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -103,4 +103,125 @@ describe Service do
end
end
end
+
+ describe "{property}_changed?" do
+ let(:service) do
+ BambooService.create(
+ project: create(:project),
+ properties: {
+ bamboo_url: 'http://gitlab.com',
+ username: 'mic',
+ password: "password"
+ }
+ )
+ end
+
+ it "returns false when the property has not been assigned a new value" do
+ service.username = "key_changed"
+ expect(service.bamboo_url_changed?).to be_falsy
+ end
+
+ it "returns true when the property has been assigned a different value" do
+ service.bamboo_url = "http://example.com"
+ expect(service.bamboo_url_changed?).to be_truthy
+ end
+
+ it "returns true when the property has been assigned a different value twice" do
+ service.bamboo_url = "http://example.com"
+ service.bamboo_url = "http://example.com"
+ expect(service.bamboo_url_changed?).to be_truthy
+ end
+
+ it "returns false when the property has been re-assigned the same value" do
+ service.bamboo_url = 'http://gitlab.com'
+ expect(service.bamboo_url_changed?).to be_falsy
+ end
+
+ it "returns false when the property has been assigned a new value then saved" do
+ service.bamboo_url = 'http://example.com'
+ service.save
+ expect(service.bamboo_url_changed?).to be_falsy
+ end
+ end
+
+ describe "{property}_touched?" do
+ let(:service) do
+ BambooService.create(
+ project: create(:project),
+ properties: {
+ bamboo_url: 'http://gitlab.com',
+ username: 'mic',
+ password: "password"
+ }
+ )
+ end
+
+ it "returns false when the property has not been assigned a new value" do
+ service.username = "key_changed"
+ expect(service.bamboo_url_touched?).to be_falsy
+ end
+
+ it "returns true when the property has been assigned a different value" do
+ service.bamboo_url = "http://example.com"
+ expect(service.bamboo_url_touched?).to be_truthy
+ end
+
+ it "returns true when the property has been assigned a different value twice" do
+ service.bamboo_url = "http://example.com"
+ service.bamboo_url = "http://example.com"
+ expect(service.bamboo_url_touched?).to be_truthy
+ end
+
+ it "returns true when the property has been re-assigned the same value" do
+ service.bamboo_url = 'http://gitlab.com'
+ expect(service.bamboo_url_touched?).to be_truthy
+ end
+
+ it "returns false when the property has been assigned a new value then saved" do
+ service.bamboo_url = 'http://example.com'
+ service.save
+ expect(service.bamboo_url_changed?).to be_falsy
+ end
+ end
+
+ describe "{property}_was" do
+ let(:service) do
+ BambooService.create(
+ project: create(:project),
+ properties: {
+ bamboo_url: 'http://gitlab.com',
+ username: 'mic',
+ password: "password"
+ }
+ )
+ end
+
+
+ it "returns nil when the property has not been assigned a new value" do
+ service.username = "key_changed"
+ expect(service.bamboo_url_was).to be_nil
+ end
+
+ it "returns the previous value when the property has been assigned a different value" do
+ service.bamboo_url = "http://example.com"
+ expect(service.bamboo_url_was).to eq('http://gitlab.com')
+ end
+
+ it "returns initial value when the property has been re-assigned the same value" do
+ service.bamboo_url = 'http://gitlab.com'
+ expect(service.bamboo_url_was).to eq('http://gitlab.com')
+ end
+
+ it "returns initial value when the property has been assigned multiple values" do
+ service.bamboo_url = "http://example.com"
+ service.bamboo_url = "http://example2.com"
+ expect(service.bamboo_url_was).to eq('http://gitlab.com')
+ end
+
+ it "returns nil when the property has been assigned a new value then saved" do
+ service.bamboo_url = 'http://example.com'
+ service.save
+ expect(service.bamboo_url_was).to be_nil
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 480950859a2..a0f78d3b336 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -54,6 +54,8 @@
# public_email :string(255) default(""), not null
# dashboard :integer default(0)
# project_view :integer default(0)
+# consumed_timestep :integer
+# layout :integer default(0)
#
require 'spec_helper'
@@ -85,10 +87,27 @@ describe User do
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) }
it { is_expected.to have_many(:identities).dependent(:destroy) }
+ it { is_expected.to have_one(:abuse_report) }
end
describe 'validations' do
- it { is_expected.to validate_presence_of(:username) }
+ describe 'username' do
+ it 'validates presence' do
+ expect(subject).to validate_presence_of(:username)
+ end
+
+ it 'rejects blacklisted names' do
+ user = build(:user, username: 'dashboard')
+
+ expect(user).not_to be_valid
+ expect(user.errors.values).to eq [['dashboard is a reserved name']]
+ end
+
+ it 'validates uniqueness' do
+ expect(subject).to validate_uniqueness_of(:username)
+ end
+ end
+
it { is_expected.to validate_presence_of(:projects_limit) }
it { is_expected.to validate_numericality_of(:projects_limit) }
it { is_expected.to allow_value(0).for(:projects_limit) }
@@ -227,6 +246,26 @@ describe User do
end
end
+ describe '#recently_sent_password_reset?' do
+ it 'is false when reset_password_sent_at is nil' do
+ user = build_stubbed(:user, reset_password_sent_at: nil)
+
+ expect(user.recently_sent_password_reset?).to eq false
+ end
+
+ it 'is false when sent more than one minute ago' do
+ user = build_stubbed(:user, reset_password_sent_at: 5.minutes.ago)
+
+ expect(user.recently_sent_password_reset?).to eq false
+ end
+
+ it 'is true when sent less than one minute ago' do
+ user = build_stubbed(:user, reset_password_sent_at: Time.now)
+
+ expect(user.recently_sent_password_reset?).to eq true
+ end
+ end
+
describe '#disable_two_factor!' do
it 'clears all 2FA-related fields' do
user = create(:user, :two_factor)
@@ -642,28 +681,28 @@ describe User do
@user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega'
end
- it "sorts users as recently_signed_in" do
+ it "sorts users by the recent sign-in time" do
expect(User.sort('recent_sign_in').first).to eq(@user)
end
- it "sorts users as late_signed_in" do
+ it "sorts users by the oldest sign-in time" do
expect(User.sort('oldest_sign_in').first).to eq(@user1)
end
- it "sorts users as recently_created" do
+ it "sorts users in descending order by their creation time" do
expect(User.sort('created_desc').first).to eq(@user)
end
- it "sorts users as late_created" do
+ it "sorts users in ascending order by their creation time" do
expect(User.sort('created_asc').first).to eq(@user1)
end
- it "sorts users by name when nil is passed" do
- expect(User.sort(nil).first).to eq(@user)
+ it "sorts users by id in descending order when nil is passed" do
+ expect(User.sort(nil).first).to eq(@user1)
end
end
- describe "#contributed_projects_ids" do
+ describe "#contributed_projects" do
subject { create(:user) }
let!(:project1) { create(:project) }
let!(:project2) { create(:project, forked_from_project: project3) }
@@ -678,15 +717,15 @@ describe User do
end
it "includes IDs for projects the user has pushed to" do
- expect(subject.contributed_projects_ids).to include(project1.id)
+ expect(subject.contributed_projects).to include(project1)
end
it "includes IDs for projects the user has had merge requests merged into" do
- expect(subject.contributed_projects_ids).to include(project3.id)
+ expect(subject.contributed_projects).to include(project3)
end
it "doesn't include IDs for unrelated projects" do
- expect(subject.contributed_projects_ids).not_to include(project2.id)
+ expect(subject.contributed_projects).not_to include(project2)
end
end
@@ -735,4 +774,30 @@ describe User do
expect(subject.recent_push).to eq(nil)
end
end
+
+ describe '#authorized_groups' do
+ let!(:user) { create(:user) }
+ let!(:private_group) { create(:group) }
+
+ before do
+ private_group.add_user(user, Gitlab::Access::MASTER)
+ end
+
+ subject { user.authorized_groups }
+
+ it { is_expected.to eq([private_group]) }
+ end
+
+ describe '#authorized_projects' do
+ let!(:user) { create(:user) }
+ let!(:private_project) { create(:project, :private) }
+
+ before do
+ private_project.team << [user, Gitlab::Access::MASTER]
+ end
+
+ subject { user.authorized_projects }
+
+ it { is_expected.to eq([private_project]) }
+ end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index dc84a14bb40..d7802d1734f 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -196,7 +196,7 @@ describe WikiPage do
end
def commit_details
- commit = { name: user.name, email: user.email, message: "test commit" }
+ { name: user.name, email: user.email, message: "test commit" }
end
def create_page(name, content)
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 4048c297013..0c19094ec54 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe API, api: true do
- include API::APIHelpers
+ include API::Helpers
include ApiHelpers
let(:user) { create(:user) }
let(:admin) { create(:admin) }
@@ -13,25 +13,25 @@ describe API, api: true do
def set_env(token_usr, identifier)
clear_env
clear_param
- env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token
- env[API::APIHelpers::SUDO_HEADER] = identifier
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token
+ env[API::Helpers::SUDO_HEADER] = identifier
end
def set_param(token_usr, identifier)
clear_env
clear_param
- params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token
- params[API::APIHelpers::SUDO_PARAM] = identifier
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token
+ params[API::Helpers::SUDO_PARAM] = identifier
end
def clear_env
- env.delete(API::APIHelpers::PRIVATE_TOKEN_HEADER)
- env.delete(API::APIHelpers::SUDO_HEADER)
+ env.delete(API::Helpers::PRIVATE_TOKEN_HEADER)
+ env.delete(API::Helpers::SUDO_HEADER)
end
def clear_param
- params.delete(API::APIHelpers::PRIVATE_TOKEN_PARAM)
- params.delete(API::APIHelpers::SUDO_PARAM)
+ params.delete(API::Helpers::PRIVATE_TOKEN_PARAM)
+ params.delete(API::Helpers::SUDO_PARAM)
end
def error!(message, status)
@@ -40,22 +40,22 @@ describe API, api: true do
describe ".current_user" do
it "should return nil for an invalid token" do
- env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
expect(current_user).to be_nil
end
it "should return nil for a user without access" do
- env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect(current_user).to be_nil
end
it "should leave user as is when sudo not specified" do
- env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
expect(current_user).to eq(user)
clear_env
- params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = user.private_token
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
expect(current_user).to eq(user)
end
diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb
new file mode 100644
index 00000000000..a28607bd240
--- /dev/null
+++ b/spec/requests/api/commit_status_spec.rb
@@ -0,0 +1,137 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:reporter) { create(:project_member, user: user, project: project, access_level: ProjectMember::REPORTER) }
+ let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
+ let(:commit) { project.repository.commit }
+ let!(:ci_commit) { project.ensure_ci_commit(commit.id) }
+ let(:commit_status) { create(:commit_status, commit: ci_commit) }
+
+ describe "GET /projects/:id/repository/commits/:sha/statuses" do
+ context "reporter user" do
+ let(:statuses_id) { json_response.map { |status| status['id'] } }
+
+ before do
+ @status1 = create(:commit_status, commit: ci_commit, status: 'running')
+ @status2 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'pending')
+ @status3 = create(:commit_status, commit: ci_commit, name: 'coverage', ref: 'develop', status: 'running', allow_failure: true)
+ @status4 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'success')
+ @status5 = create(:commit_status, commit: ci_commit, ref: 'develop', status: 'success')
+ @status6 = create(:commit_status, commit: ci_commit, status: 'success')
+ end
+
+ it "should return latest commit statuses" do
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user)
+ expect(response.status).to eq(200)
+
+ expect(json_response).to be_an Array
+ expect(statuses_id).to contain_exactly(@status3.id, @status4.id, @status5.id, @status6.id)
+ json_response.sort_by!{ |status| status['id'] }
+ expect(json_response.map{ |status| status['allow_failure'] }).to eq([true, false, false, false])
+ end
+
+ it "should return all commit statuses" do
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?all=1", user)
+ expect(response.status).to eq(200)
+
+ expect(json_response).to be_an Array
+ expect(statuses_id).to contain_exactly(@status1.id, @status2.id, @status3.id, @status4.id, @status5.id, @status6.id)
+ end
+
+ it "should return latest commit statuses for specific ref" do
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?ref=develop", user)
+ expect(response.status).to eq(200)
+
+ expect(json_response).to be_an Array
+ expect(statuses_id).to contain_exactly(@status3.id, @status5.id)
+ end
+
+ it "should return latest commit statuses for specific name" do
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?name=coverage", user)
+ expect(response.status).to eq(200)
+
+ expect(json_response).to be_an Array
+ expect(statuses_id).to contain_exactly(@status3.id, @status4.id)
+ end
+ end
+
+ context "guest user" do
+ it "should not return project commits" do
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user2)
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context "unauthorized user" do
+ it "should not return project commits" do
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses")
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/statuses/:sha' do
+ let(:post_url) { "/projects/#{project.id}/statuses/#{commit.id}" }
+
+ context 'reporter user' do
+ context 'should create commit status' do
+ it 'with only required parameters' do
+ post api(post_url, user), state: 'success'
+ expect(response.status).to eq(201)
+ expect(json_response['sha']).to eq(commit.id)
+ expect(json_response['status']).to eq('success')
+ expect(json_response['name']).to eq('default')
+ expect(json_response['ref']).to be_nil
+ expect(json_response['target_url']).to be_nil
+ expect(json_response['description']).to be_nil
+ end
+
+ it 'with all optional parameters' do
+ post api(post_url, user), state: 'success', context: 'coverage', ref: 'develop', target_url: 'url', description: 'test'
+ expect(response.status).to eq(201)
+ expect(json_response['sha']).to eq(commit.id)
+ expect(json_response['status']).to eq('success')
+ expect(json_response['name']).to eq('coverage')
+ expect(json_response['ref']).to eq('develop')
+ expect(json_response['target_url']).to eq('url')
+ expect(json_response['description']).to eq('test')
+ end
+ end
+
+ context 'should not create commit status' do
+ it 'with invalid state' do
+ post api(post_url, user), state: 'invalid'
+ expect(response.status).to eq(400)
+ end
+
+ it 'without state' do
+ post api(post_url, user)
+ expect(response.status).to eq(400)
+ end
+
+ it 'invalid commit' do
+ post api("/projects/#{project.id}/statuses/invalid_sha", user), state: 'running'
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ context 'guest user' do
+ it 'should not create commit status' do
+ post api(post_url, user2)
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not create commit status' do
+ post api(post_url)
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index a1c248c636e..49acc3368f4 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -47,6 +47,19 @@ describe API::API, api: true do
get api("/projects/#{project.id}/repository/commits/invalid_sha", user)
expect(response.status).to eq(404)
end
+
+ it "should return not_found for CI status" do
+ get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+ expect(response.status).to eq(200)
+ expect(json_response['status']).to eq('not_found')
+ end
+
+ it "should return status for CI" do
+ ci_commit = project.ensure_ci_commit(project.repository.commit.sha)
+ get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+ expect(response.status).to eq(200)
+ expect(json_response['status']).to eq(ci_commit.status)
+ end
end
context "unauthorized user" do
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 042e6352567..8efa09f75fd 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -19,6 +19,7 @@ describe API::API, api: true do
expect(response.status).to eq(200)
expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq('popen.rb')
+ expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index aff109a9424..667f0dbea5c 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -47,7 +47,7 @@ describe API::API, api: true do
name: 'Foo',
color: '#FFAA'
expect(response.status).to eq(400)
- expect(json_response['message']['color']).to eq(['is invalid'])
+ expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
it 'should return 400 for too long color code' do
@@ -55,7 +55,7 @@ describe API::API, api: true do
name: 'Foo',
color: '#FFAAFFFF'
expect(response.status).to eq(400)
- expect(json_response['message']['color']).to eq(['is invalid'])
+ expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
it 'should return 400 for invalid name' do
@@ -151,12 +151,12 @@ describe API::API, api: true do
expect(json_response['message']['title']).to eq(['is invalid'])
end
- it 'should return 400 for invalid name' do
+ it 'should return 400 when color code is too short' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FF'
expect(response.status).to eq(400)
- expect(json_response['message']['color']).to eq(['is invalid'])
+ expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
it 'should return 400 for too long color code' do
@@ -164,7 +164,7 @@ describe API::API, api: true do
name: 'Foo',
color: '#FFAAFFFF'
expect(response.status).to eq(400)
- expect(json_response['message']['color']).to eq(['is invalid'])
+ expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 35b3d3e296a..c6d3aef0af9 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -131,6 +131,23 @@ describe API::API, api: true do
end
end
+ describe 'GET /projects/:id/merge_request/:merge_request_id/commits' do
+ context 'valid merge request' do
+ before { get api("/projects/#{project.id}/merge_request/#{merge_request.id}/commits", user) }
+ let(:commit) { merge_request.commits.first }
+
+ it { expect(response.status).to eq 200 }
+ it { expect(json_response.size).to eq(merge_request.commits.size) }
+ it { expect(json_response.first['id']).to eq(commit.id) }
+ it { expect(json_response.first['title']).to eq(commit.title) }
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get api("/projects/#{project.id}/merge_request/999/commits", user)
+ expect(response.status).to eq(404)
+ end
+ end
+
describe 'GET /projects/:id/merge_request/:merge_request_id/changes' do
it 'should return the change information of the merge_request' do
get api("/projects/#{project.id}/merge_request/#{merge_request.id}/changes", user)
@@ -379,9 +396,14 @@ describe API::API, api: true do
describe "POST /projects/:id/merge_request/:merge_request_id/comments" do
it "should return comment" do
+ original_count = merge_request.notes.size
+
post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user), note: "My comment"
expect(response.status).to eq(201)
expect(json_response['note']).to eq('My comment')
+ expect(json_response['author']['name']).to eq(user.name)
+ expect(json_response['author']['username']).to eq(user.username)
+ expect(merge_request.notes.size).to eq(original_count + 1)
end
it "should return 400 if note is missing" do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 580bbec77d1..24b765f4979 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -88,8 +88,11 @@ describe API::API, api: true do
end
it 'returns projects in the correct order when ci_enabled_first parameter is passed' do
- [project, project2, project3].each{ |project| project.build_missing_services }
- project2.gitlab_ci_service.update(active: true)
+ [project, project2, project3].each do |project|
+ project.builds_enabled = false
+ project.build_missing_services
+ end
+ project2.builds_enabled = true
get api('/projects', user), { ci_enabled_first: 'true' }
expect(response.status).to eq(200)
expect(json_response).to be_an Array
@@ -386,14 +389,30 @@ describe API::API, api: true do
describe 'GET /projects/:id/events' do
before { project_member2 }
- it 'should return a project events' do
- get api("/projects/#{project.id}/events", user)
- expect(response.status).to eq(200)
- json_event = json_response.first
+ context 'valid request' do
+ before do
+ note = create(:note_on_issue, note: 'What an awesome day!', project: project)
+ EventCreateService.new.leave_note(note, note.author)
+ get api("/projects/#{project.id}/events", user)
+ end
+
+ it { expect(response.status).to eq(200) }
- expect(json_event['action_name']).to eq('joined')
- expect(json_event['project_id'].to_i).to eq(project.id)
- expect(json_event['author_username']).to eq(user3.username)
+ context 'joined event' do
+ let(:json_event) { json_response[1] }
+
+ it { expect(json_event['action_name']).to eq('joined') }
+ it { expect(json_event['project_id'].to_i).to eq(project.id) }
+ it { expect(json_event['author_username']).to eq(user3.username) }
+ it { expect(json_event['author']['name']).to eq(user3.name) }
+ end
+
+ context 'comment event' do
+ let(:json_event) { json_response.first }
+
+ it { expect(json_event['action_name']).to eq('commented on') }
+ it { expect(json_event['note']['body']).to eq('What an awesome day!') }
+ end
end
it 'should return a 404 error if not found' do
@@ -606,28 +625,42 @@ describe API::API, api: true do
describe 'DELETE /projects/:id/fork' do
- it "shouldn't available for non admin users" do
+ it "shouldn't be visible to users outside group" do
delete api("/projects/#{project_fork_target.id}/fork", user)
- expect(response.status).to eq(403)
+ expect(response.status).to eq(404)
end
- it 'should make forked project unforked' do
- post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
- project_fork_target.reload
- expect(project_fork_target.forked_from_project).not_to be_nil
- expect(project_fork_target.forked?).to be_truthy
- delete api("/projects/#{project_fork_target.id}/fork", admin)
- expect(response.status).to eq(200)
- project_fork_target.reload
- expect(project_fork_target.forked_from_project).to be_nil
- expect(project_fork_target.forked?).not_to be_truthy
- end
+ context 'when users belong to project group' do
+ let(:project_fork_target) { create(:project, group: create(:group)) }
- it 'should be idempotent if not forked' do
- expect(project_fork_target.forked_from_project).to be_nil
- delete api("/projects/#{project_fork_target.id}/fork", admin)
- expect(response.status).to eq(200)
- expect(project_fork_target.reload.forked_from_project).to be_nil
+ before do
+ project_fork_target.group.add_owner user
+ project_fork_target.group.add_developer user2
+ end
+
+ it 'should be forbidden to non-owner users' do
+ delete api("/projects/#{project_fork_target.id}/fork", user2)
+ expect(response.status).to eq(403)
+ end
+
+ it 'should make forked project unforked' do
+ post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project).not_to be_nil
+ expect(project_fork_target.forked?).to be_truthy
+ delete api("/projects/#{project_fork_target.id}/fork", admin)
+ expect(response.status).to eq(200)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project).to be_nil
+ expect(project_fork_target.forked?).not_to be_truthy
+ end
+
+ it 'should be idempotent if not forked' do
+ expect(project_fork_target.forked_from_project).to be_nil
+ delete api("/projects/#{project_fork_target.id}/fork", admin)
+ expect(response.status).to eq(200)
+ expect(project_fork_target.reload.forked_from_project).to be_nil
+ end
end
end
end
@@ -709,6 +742,18 @@ describe API::API, api: true do
end
end
+ it 'should update visibility_level from public to private' do
+ project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
+
+ project_param = { public: false }
+ put api("/projects/#{project3.id}", user), project_param
+ expect(response.status).to eq(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
it 'should not update name to existing name' do
project_param = { name: project3.name }
put api("/projects/#{project.id}", user), project_param
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 09a79553f72..4911cdd9da6 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -11,81 +11,6 @@ describe API::API, api: true do
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
- describe "GET /projects/:id/repository/tags" do
- it "should return an array of project tags" do
- get api("/projects/#{project.id}/repository/tags", user)
- expect(response.status).to eq(200)
- expect(json_response).to be_an Array
- expect(json_response.first['name']).to eq(project.repo.tags.sort_by(&:name).reverse.first.name)
- end
- end
-
- describe 'POST /projects/:id/repository/tags' do
- context 'lightweight tags' do
- it 'should create a new tag' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v7.0.1',
- ref: 'master'
-
- expect(response.status).to eq(201)
- expect(json_response['name']).to eq('v7.0.1')
- end
- end
-
- context 'annotated tag' do
- it 'should create a new annotated tag' do
- # Identity must be set in .gitconfig to create annotated tag.
- repo_path = project.repository.path_to_repo
- system(*%W(git --git-dir=#{repo_path} config user.name #{user.name}))
- system(*%W(git --git-dir=#{repo_path} config user.email #{user.email}))
-
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v7.1.0',
- ref: 'master',
- message: 'Release 7.1.0'
-
- expect(response.status).to eq(201)
- expect(json_response['name']).to eq('v7.1.0')
- expect(json_response['message']).to eq('Release 7.1.0')
- end
- end
-
- it 'should deny for user without push access' do
- post api("/projects/#{project.id}/repository/tags", user2),
- tag_name: 'v1.9.0',
- ref: '621491c677087aa243f165eab467bfdfbee00be1'
- expect(response.status).to eq(403)
- end
-
- it 'should return 400 if tag name is invalid' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v 1.0.0',
- ref: 'master'
- expect(response.status).to eq(400)
- expect(json_response['message']).to eq('Tag name invalid')
- end
-
- it 'should return 400 if tag already exists' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v8.0.0',
- ref: 'master'
- expect(response.status).to eq(201)
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v8.0.0',
- ref: 'master'
- expect(response.status).to eq(400)
- expect(json_response['message']).to eq('Tag already exists')
- end
-
- it 'should return 400 if ref name is invalid' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'mytag',
- ref: 'foo'
- expect(response.status).to eq(400)
- expect(json_response['message']).to eq('Invalid reference name')
- end
- end
-
describe "GET /projects/:id/repository/tree" do
context "authorized user" do
before { project.team << [user2, :reporter] }
@@ -166,24 +91,21 @@ describe API::API, api: true do
get api("/projects/#{project.id}/repository/archive", user)
repo_name = project.repository.name.gsub("\.git", "")
expect(response.status).to eq(200)
- expect(response.headers['Content-Disposition']).to match(/filename\=\"#{repo_name}\-[^\.]+\.tar.gz\"/)
- expect(response.content_type).to eq(MIME::Types.type_for('file.tar.gz').first.content_type)
+ expect(json_response['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
end
it "should get the archive.zip" do
get api("/projects/#{project.id}/repository/archive.zip", user)
repo_name = project.repository.name.gsub("\.git", "")
expect(response.status).to eq(200)
- expect(response.headers['Content-Disposition']).to match(/filename\=\"#{repo_name}\-[^\.]+\.zip\"/)
- expect(response.content_type).to eq(MIME::Types.type_for('file.zip').first.content_type)
+ expect(json_response['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
end
it "should get the archive.tar.bz2" do
get api("/projects/#{project.id}/repository/archive.tar.bz2", user)
repo_name = project.repository.name.gsub("\.git", "")
expect(response.status).to eq(200)
- expect(response.headers['Content-Disposition']).to match(/filename\=\"#{repo_name}\-[^\.]+\.tar.bz2\"/)
- expect(response.content_type).to eq(MIME::Types.type_for('file.tar.bz2').first.content_type)
+ expect(json_response['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
end
it "should return 404 for invalid sha" do
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 9aa60826f21..b180d2fec77 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -3,6 +3,8 @@ require "spec_helper"
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:user2) { create(:user) }
let(:project) {create(:project, creator_id: user.id, namespace: user.namespace) }
Service.available_services_names.each do |service|
@@ -44,6 +46,7 @@ describe API::API, api: true do
delete api("/projects/#{project.id}/services/#{dashed_service}", user)
expect(response.status).to eq(200)
+ project.send(service_method).reload
expect(project.send(service_method).activated?).to be_falsey
end
end
@@ -51,11 +54,40 @@ describe API::API, api: true do
describe "GET /projects/:id/services/#{service.dasherize}" do
include_context service
- it "should get #{service} settings" do
+ # inject some properties into the service
+ before do
+ project.build_missing_services
+ service_object = project.send(service_method)
+ service_object.properties = service_attrs
+ service_object.save
+ end
+
+ it 'should return authentication error when unauthenticated' do
+ get api("/projects/#{project.id}/services/#{dashed_service}")
+ expect(response.status).to eq(401)
+ end
+
+ it "should return all properties of service #{service} when authenticated as admin" do
+ get api("/projects/#{project.id}/services/#{dashed_service}", admin)
+
+ expect(response.status).to eq(200)
+ expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list.map)
+ end
+
+ it "should return properties of service #{service} other than passwords when authenticated as project owner" do
get api("/projects/#{project.id}/services/#{dashed_service}", user)
expect(response.status).to eq(200)
+ expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list_without_passwords)
end
+
+ it "should return error when authenticated but not a project owner" do
+ project.team << [user2, :developer]
+ get api("/projects/#{project.id}/services/#{dashed_service}", user2)
+
+ expect(response.status).to eq(403)
+ end
+
end
end
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
new file mode 100644
index 00000000000..17f2643fd45
--- /dev/null
+++ b/spec/requests/api/tags_spec.rb
@@ -0,0 +1,196 @@
+require 'spec_helper'
+require 'mime/types'
+
+describe API::API, api: true do
+ include ApiHelpers
+ include RepoHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
+ let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
+
+ describe "GET /projects/:id/repository/tags" do
+ let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+ let(:description) { 'Awesome release!' }
+
+ context 'without releases' do
+ it "should return an array of project tags" do
+ get api("/projects/#{project.id}/repository/tags", user)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
+ end
+ end
+
+ context 'with releases' do
+ before do
+ release = project.releases.find_or_initialize_by(tag: tag_name)
+ release.update_attributes(description: description)
+ end
+
+ it "should return an array of project tags with release info" do
+ get api("/projects/#{project.id}/repository/tags", user)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
+ expect(json_response.first['release']['description']).to eq(description)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/repository/tags' do
+ context 'lightweight tags' do
+ it 'should create a new tag' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v7.0.1',
+ ref: 'master'
+
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq('v7.0.1')
+ end
+ end
+
+ context 'lightweight tags with release notes' do
+ it 'should create a new tag' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v7.0.1',
+ ref: 'master',
+ release_description: 'Wow'
+
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq('v7.0.1')
+ expect(json_response['release']['description']).to eq('Wow')
+ end
+ end
+
+ context 'annotated tag' do
+ it 'should create a new annotated tag' do
+ # Identity must be set in .gitconfig to create annotated tag.
+ repo_path = project.repository.path_to_repo
+ system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name}))
+ system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.email #{user.email}))
+
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v7.1.0',
+ ref: 'master',
+ message: 'Release 7.1.0'
+
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq('v7.1.0')
+ expect(json_response['message']).to eq('Release 7.1.0')
+ end
+ end
+
+ it 'should deny for user without push access' do
+ post api("/projects/#{project.id}/repository/tags", user2),
+ tag_name: 'v1.9.0',
+ ref: '621491c677087aa243f165eab467bfdfbee00be1'
+ expect(response.status).to eq(403)
+ end
+
+ it 'should return 400 if tag name is invalid' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v 1.0.0',
+ ref: 'master'
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('Tag name invalid')
+ end
+
+ it 'should return 400 if tag already exists' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v8.0.0',
+ ref: 'master'
+ expect(response.status).to eq(201)
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v8.0.0',
+ ref: 'master'
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('Tag already exists')
+ end
+
+ it 'should return 400 if ref name is invalid' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'mytag',
+ ref: 'foo'
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('Invalid reference name')
+ end
+ end
+
+ describe 'POST /projects/:id/repository/tags/:tag_name/release' do
+ let(:tag_name) { project.repository.tag_names.first }
+ let(:description) { 'Awesome release!' }
+
+ it 'should create description for existing git tag' do
+ post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
+ description: description
+
+ expect(response.status).to eq(201)
+ expect(json_response['tag_name']).to eq(tag_name)
+ expect(json_response['description']).to eq(description)
+ end
+
+ it 'should return 404 if the tag does not exist' do
+ post api("/projects/#{project.id}/repository/tags/foobar/release", user),
+ description: description
+
+ expect(response.status).to eq(404)
+ expect(json_response['message']).to eq('Tag does not exist')
+ end
+
+ context 'on tag with existing release' do
+ before do
+ release = project.releases.find_or_initialize_by(tag: tag_name)
+ release.update_attributes(description: description)
+ end
+
+ it 'should return 409 if there is already a release' do
+ post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
+ description: description
+
+ expect(response.status).to eq(409)
+ expect(json_response['message']).to eq('Release already exists')
+ end
+ end
+ end
+
+ describe 'PUT id/repository/tags/:tag_name/release' do
+ let(:tag_name) { project.repository.tag_names.first }
+ let(:description) { 'Awesome release!' }
+ let(:new_description) { 'The best release!' }
+
+ context 'on tag with existing release' do
+ before do
+ release = project.releases.find_or_initialize_by(tag: tag_name)
+ release.update_attributes(description: description)
+ end
+
+ it 'should update the release description' do
+ put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
+ description: new_description
+
+ expect(response.status).to eq(200)
+ expect(json_response['tag_name']).to eq(tag_name)
+ expect(json_response['description']).to eq(new_description)
+ end
+ end
+
+ it 'should return 404 if the tag does not exist' do
+ put api("/projects/#{project.id}/repository/tags/foobar/release", user),
+ description: new_description
+
+ expect(response.status).to eq(404)
+ expect(json_response['message']).to eq('Tag does not exist')
+ end
+
+ it 'should return 404 if the release does not exist' do
+ put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
+ description: new_description
+
+ expect(response.status).to eq(404)
+ expect(json_response['message']).to eq('Release does not exist')
+ end
+ end
+end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index d26a300ed82..2f609c63330 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -153,7 +153,7 @@ describe API::API, api: true do
expect(json_response['message']['projects_limit']).
to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username']).
- to eq([Gitlab::Regex.send(:namespace_regex_message)])
+ to eq([Gitlab::Regex.namespace_regex_message])
end
it "shouldn't available for non admin users" do
@@ -296,7 +296,7 @@ describe API::API, api: true do
expect(json_response['message']['projects_limit']).
to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username']).
- to eq([Gitlab::Regex.send(:namespace_regex_message)])
+ to eq([Gitlab::Regex.namespace_regex_message])
end
context "with existing user" do
@@ -343,8 +343,9 @@ describe API::API, api: true do
end.to change{ user.keys.count }.by(1)
end
- it "should raise error for invalid ID" do
- expect{post api("/users/ASDF/keys", admin) }.to raise_error(ActionController::RoutingError)
+ it "should return 405 for invalid ID" do
+ post api("/users/ASDF/keys", admin)
+ expect(response.status).to eq(405)
end
end
@@ -374,9 +375,9 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(key.title)
end
- it "should return 404 for invalid ID" do
+ it "should return 405 for invalid ID" do
get api("/users/ASDF/keys", admin)
- expect(response.status).to eq(404)
+ expect(response.status).to eq(405)
end
end
end
@@ -434,7 +435,8 @@ describe API::API, api: true do
end
it "should raise error for invalid ID" do
- expect{post api("/users/ASDF/emails", admin) }.to raise_error(ActionController::RoutingError)
+ post api("/users/ASDF/emails", admin)
+ expect(response.status).to eq(405)
end
end
@@ -465,7 +467,8 @@ describe API::API, api: true do
end
it "should raise error for invalid ID" do
- expect{put api("/users/ASDF/emails", admin) }.to raise_error(ActionController::RoutingError)
+ put api("/users/ASDF/emails", admin)
+ expect(response.status).to eq(405)
end
end
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index c25d1823306..c2be045099d 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -5,10 +5,16 @@ describe Ci::API::API do
let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
let(:project) { FactoryGirl.create(:ci_project) }
+ let(:gl_project) { project.gl_project }
+
+ before do
+ stub_ci_commit_to_return_yaml_file
+ end
describe "Builds API for runners" do
let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") }
let(:shared_project) { FactoryGirl.create(:ci_project, name: "SharedProject") }
+ let(:shared_gl_project) { shared_project.gl_project }
before do
FactoryGirl.create :ci_runner_project, project_id: project.id, runner_id: runner.id
@@ -16,8 +22,8 @@ describe Ci::API::API do
describe "POST /builds/register" do
it "should start a build" do
- commit = FactoryGirl.create(:ci_commit, project: project)
- commit.create_builds
+ commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+ commit.create_builds('master', false, nil)
build = commit.builds.first
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -34,8 +40,8 @@ describe Ci::API::API do
end
it "should return 404 error if no builds for specific runner" do
- commit = FactoryGirl.create(:ci_commit, project: shared_project)
- FactoryGirl.create(:ci_build, commit: commit, status: 'pending' )
+ commit = FactoryGirl.create(:ci_commit, gl_project: shared_gl_project)
+ FactoryGirl.create(:ci_build, commit: commit, status: 'pending')
post ci_api("/builds/register"), token: runner.token
@@ -43,8 +49,8 @@ describe Ci::API::API do
end
it "should return 404 error if no builds for shared runner" do
- commit = FactoryGirl.create(:ci_commit, project: project)
- FactoryGirl.create(:ci_build, commit: commit, status: 'pending' )
+ commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+ FactoryGirl.create(:ci_build, commit: commit, status: 'pending')
post ci_api("/builds/register"), token: shared_runner.token
@@ -52,8 +58,8 @@ describe Ci::API::API do
end
it "returns options" do
- commit = FactoryGirl.create(:ci_commit, project: project)
- commit.create_builds
+ commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+ commit.create_builds('master', false, nil)
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -62,31 +68,36 @@ describe Ci::API::API do
end
it "returns variables" do
- commit = FactoryGirl.create(:ci_commit, project: project)
- commit.create_builds
+ commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+ commit.create_builds('master', false, nil)
project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
expect(response.status).to eq(201)
expect(json_response["variables"]).to eq([
+ { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
+ { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
{ "key" => "DB_NAME", "value" => "postgres", "public" => true },
- { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
+ { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }
])
end
it "returns variables for triggers" do
trigger = FactoryGirl.create(:ci_trigger, project: project)
- commit = FactoryGirl.create(:ci_commit, project: project)
+ commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger)
- commit.create_builds(trigger_request)
+ commit.create_builds('master', false, nil, trigger_request)
project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
expect(response.status).to eq(201)
expect(json_response["variables"]).to eq([
+ { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
+ { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+ { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
{ "key" => "DB_NAME", "value" => "postgres", "public" => true },
{ "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
{ "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false },
@@ -95,7 +106,7 @@ describe Ci::API::API do
end
describe "PUT /builds/:id" do
- let(:commit) { FactoryGirl.create(:ci_commit, project: project)}
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project)}
let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) }
it "should update a running build" do
@@ -111,5 +122,191 @@ describe Ci::API::API do
expect(build.reload.trace).to eq 'hello_world'
end
end
+
+ context "Artifacts" do
+ let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
+ let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) }
+ let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") }
+ let(:post_url) { ci_api("/builds/#{build.id}/artifacts") }
+ let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") }
+ let(:get_url) { ci_api("/builds/#{build.id}/artifacts") }
+ let(:headers) { { "GitLab-Workhorse" => "1.0" } }
+ let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.project.token) }
+
+ describe "POST /builds/:id/artifacts/authorize" do
+ context "should authorize posting artifact to running build" do
+ before do
+ build.run!
+ end
+
+ it "using token as parameter" do
+ post authorize_url, { token: build.project.token }, headers
+ expect(response.status).to eq(200)
+ expect(json_response["TempPath"]).to_not be_nil
+ end
+
+ it "using token as header" do
+ post authorize_url, {}, headers_with_token
+ expect(response.status).to eq(200)
+ expect(json_response["TempPath"]).to_not be_nil
+ end
+ end
+
+ context "should fail to post too large artifact" do
+ before do
+ build.run!
+ end
+
+ it "using token as parameter" do
+ stub_application_setting(max_artifacts_size: 0)
+ post authorize_url, { token: build.project.token, filesize: 100 }, headers
+ expect(response.status).to eq(413)
+ end
+
+ it "using token as header" do
+ stub_application_setting(max_artifacts_size: 0)
+ post authorize_url, { filesize: 100 }, headers_with_token
+ expect(response.status).to eq(413)
+ end
+ end
+
+ context "should get denied" do
+ it do
+ post authorize_url, { token: 'invalid', filesize: 100 }
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ describe "POST /builds/:id/artifacts" do
+ context "Disable sanitizer" do
+ before do
+ # by configuring this path we allow to pass temp file from any path
+ allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
+ end
+
+ context "should post artifact to running build" do
+ before do
+ build.run!
+ end
+
+ it "uses regual file post" do
+ upload_artifacts(file_upload, headers_with_token, false)
+ expect(response.status).to eq(201)
+ expect(json_response["artifacts_file"]["filename"]).to eq(file_upload.original_filename)
+ end
+
+ it "uses accelerated file post" do
+ upload_artifacts(file_upload, headers_with_token, true)
+ expect(response.status).to eq(201)
+ expect(json_response["artifacts_file"]["filename"]).to eq(file_upload.original_filename)
+ end
+
+ it "updates artifact" do
+ upload_artifacts(file_upload, headers_with_token)
+ upload_artifacts(file_upload2, headers_with_token)
+ expect(response.status).to eq(201)
+ expect(json_response["artifacts_file"]["filename"]).to eq(file_upload2.original_filename)
+ end
+ end
+
+ context "should fail to post too large artifact" do
+ before do
+ build.run!
+ end
+
+ it do
+ stub_application_setting(max_artifacts_size: 0)
+ upload_artifacts(file_upload, headers_with_token)
+ expect(response.status).to eq(413)
+ end
+ end
+
+ context "should fail to post artifacts without file" do
+ before do
+ build.run!
+ end
+
+ it do
+ post post_url, {}, headers_with_token
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context "should fail to post artifacts without GitLab-Workhorse" do
+ before do
+ build.run!
+ end
+
+ it do
+ post post_url, { token: build.project.token }, {}
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ context "should fail to post artifacts for outside of tmp path" do
+ before do
+ # by configuring this path we allow to pass file from @tmpdir only
+ # but all temporary files are stored in system tmp directory
+ @tmpdir = Dir.mktmpdir
+ allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
+ build.run!
+ end
+
+ after do
+ FileUtils.remove_entry @tmpdir
+ end
+
+ it do
+ upload_artifacts(file_upload, headers_with_token)
+ expect(response.status).to eq(400)
+ end
+ end
+
+ def upload_artifacts(file, headers = {}, accelerated = true)
+ if accelerated
+ post post_url, {
+ 'file.path' => file.path,
+ 'file.name' => file.original_filename
+ }, headers
+ else
+ post post_url, { file: file }, headers
+ end
+ end
+ end
+
+ describe "DELETE /builds/:id/artifacts" do
+ before do
+ build.run!
+ post delete_url, token: build.project.token, file: file_upload
+ end
+
+ it "should delete artifact build" do
+ build.success
+ delete delete_url, token: build.project.token
+ expect(response.status).to eq(200)
+ end
+ end
+
+ describe "GET /builds/:id/artifacts" do
+ before do
+ build.run!
+ end
+
+ it "should download artifact" do
+ build.update_attributes(artifacts_file: file_upload)
+ get get_url, token: build.project.token
+ expect(response.status).to eq(200)
+ end
+
+ it "should fail to download if no artifact uploaded" do
+ get get_url, token: build.project.token
+ expect(response.status).to eq(404)
+ end
+ end
+ end
end
end
diff --git a/spec/requests/ci/api/commits_spec.rb b/spec/requests/ci/api/commits_spec.rb
index e89b6651499..aa51ba95bca 100644
--- a/spec/requests/ci/api/commits_spec.rb
+++ b/spec/requests/ci/api/commits_spec.rb
@@ -4,7 +4,8 @@ describe Ci::API::API, 'Commits' do
include ApiHelpers
let(:project) { FactoryGirl.create(:ci_project) }
- let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
+ let(:gl_project) { project.gl_project }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
let(:options) do
{
@@ -43,8 +44,7 @@ describe Ci::API::API, 'Commits' do
"email" => "jordi@softcatala.org",
}
}
- ],
- ci_yaml_file: gitlab_ci_yaml
+ ]
}
end
diff --git a/spec/requests/ci/api/projects_spec.rb b/spec/requests/ci/api/projects_spec.rb
index 409f47fa448..893fd168d3e 100644
--- a/spec/requests/ci/api/projects_spec.rb
+++ b/spec/requests/ci/api/projects_spec.rb
@@ -41,8 +41,8 @@ describe Ci::API::API do
describe "GET /projects/owned" do
let!(:gl_project1) {FactoryGirl.create(:empty_project, namespace: user.namespace)}
let!(:gl_project2) {FactoryGirl.create(:empty_project, namespace: user.namespace)}
- let!(:project1) { FactoryGirl.create(:ci_project, gl_project: gl_project1) }
- let!(:project2) { FactoryGirl.create(:ci_project, gl_project: gl_project2) }
+ let!(:project1) { gl_project1.ensure_gitlab_ci_project }
+ let!(:project2) { gl_project2.ensure_gitlab_ci_project }
before do
project1.gl_project.team << [user, :developer]
@@ -134,7 +134,7 @@ describe Ci::API::API do
describe "PUT /projects/:id" do
let!(:project) { FactoryGirl.create(:ci_project) }
- let!(:project_info) { { name: "An updated name!" } }
+ let!(:project_info) { { default_ref: "develop" } }
before do
options.merge!(project_info)
@@ -144,7 +144,7 @@ describe Ci::API::API do
project.gl_project.team << [user, :master]
put ci_api("/projects/#{project.id}"), options
expect(response.status).to eq(200)
- expect(json_response["name"]).to eq(project_info[:name])
+ expect(json_response["default_ref"]).to eq(project_info[:default_ref])
end
it "fails to update a non-existing project" do
@@ -180,89 +180,53 @@ describe Ci::API::API do
end
end
- describe "POST /projects" do
- let(:project_info) do
- {
- name: "My project",
- gitlab_id: 1,
- path: "testing/testing",
- ssh_url_to_repo: "ssh://example.com/testing/testing.git"
- }
- end
-
- let(:invalid_project_info) { {} }
+ describe "POST /projects/:id/runners/:id" do
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:runner) { FactoryGirl.create(:ci_runner) }
- context "with valid project info" do
- before do
- options.merge!(project_info)
- end
+ it "should add the project to the runner" do
+ project.gl_project.team << [user, :master]
+ post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ expect(response.status).to eq(201)
- it "should create a project with valid data" do
- post ci_api("/projects"), options
- expect(response.status).to eq(201)
- expect(json_response['name']).to eq(project_info[:name])
- end
+ project.reload
+ expect(project.runners.first.id).to eq(runner.id)
end
- context "with invalid project info" do
- before do
- options.merge!(invalid_project_info)
- end
+ it "should fail if it tries to link a non-existing project or runner" do
+ post ci_api("/projects/#{project.id}/runners/non-existing"), options
+ expect(response.status).to eq(404)
- it "should error with invalid data" do
- post ci_api("/projects"), options
- expect(response.status).to eq(400)
- end
+ post ci_api("/projects/non-existing/runners/#{runner.id}"), options
+ expect(response.status).to eq(404)
end
- describe "POST /projects/:id/runners/:id" do
- let(:project) { FactoryGirl.create(:ci_project) }
- let(:runner) { FactoryGirl.create(:ci_runner) }
-
- it "should add the project to the runner" do
- project.gl_project.team << [user, :master]
- post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
- expect(response.status).to eq(201)
-
- project.reload
- expect(project.runners.first.id).to eq(runner.id)
- end
-
- it "should fail if it tries to link a non-existing project or runner" do
- post ci_api("/projects/#{project.id}/runners/non-existing"), options
- expect(response.status).to eq(404)
-
- post ci_api("/projects/non-existing/runners/#{runner.id}"), options
- expect(response.status).to eq(404)
- end
-
- it "non-manager is not authorized" do
- allow_any_instance_of(User).to receive(:can_manage_project?).and_return(false)
- post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
- expect(response.status).to eq(401)
- end
+ it "non-manager is not authorized" do
+ allow_any_instance_of(User).to receive(:can_manage_project?).and_return(false)
+ post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ expect(response.status).to eq(401)
end
+ end
- describe "DELETE /projects/:id/runners/:id" do
- let(:project) { FactoryGirl.create(:ci_project) }
- let(:runner) { FactoryGirl.create(:ci_runner) }
+ describe "DELETE /projects/:id/runners/:id" do
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:runner) { FactoryGirl.create(:ci_runner) }
- it "should remove the project from the runner" do
- project.gl_project.team << [user, :master]
- post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ it "should remove the project from the runner" do
+ project.gl_project.team << [user, :master]
+ post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
- expect(project.runners).to be_present
- delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
- expect(response.status).to eq(200)
+ expect(project.runners).to be_present
+ delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ expect(response.status).to eq(200)
- project.reload
- expect(project.runners).to be_empty
- end
+ project.reload
+ expect(project.runners).to be_empty
+ end
- it "non-manager is not authorized" do
- delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
- expect(response.status).to eq(401)
- end
+ it "non-manager is not authorized" do
+ delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ expect(response.status).to eq(401)
end
end
end
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index ff6fdbdd6f1..a2b436d5811 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -5,7 +5,8 @@ describe Ci::API::API do
describe 'POST /projects/:project_id/refs/:ref/trigger' do
let!(:trigger_token) { 'secure token' }
- let!(:project) { FactoryGirl.create(:ci_project) }
+ let!(:gl_project) { FactoryGirl.create(:project) }
+ let!(:project) { gl_project.ensure_gitlab_ci_project }
let!(:project2) { FactoryGirl.create(:ci_project) }
let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) }
let(:options) do
@@ -14,6 +15,10 @@ describe Ci::API::API do
}
end
+ before do
+ stub_ci_commit_to_return_yaml_file
+ end
+
context 'Handles errors' do
it 'should return bad request if token is missing' do
post ci_api("/projects/#{project.id}/refs/master/trigger")
@@ -32,15 +37,13 @@ describe Ci::API::API do
end
context 'Have a commit' do
- before do
- @commit = FactoryGirl.create(:ci_commit, project: project)
- end
+ let(:commit) { project.commits.last }
it 'should create builds' do
post ci_api("/projects/#{project.id}/refs/master/trigger"), options
expect(response.status).to eq(201)
- @commit.builds.reload
- expect(@commit.builds.size).to eq(2)
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
end
it 'should return bad request with no builds created if there\'s no commit for that ref' do
@@ -69,8 +72,8 @@ describe Ci::API::API do
it 'create trigger request with variables' do
post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: variables)
expect(response.status).to eq(201)
- @commit.builds.reload
- expect(@commit.builds.first.trigger_request.variables).to eq(variables)
+ commit.builds.reload
+ expect(commit.builds.first.trigger_request.variables).to eq(variables)
end
end
end
diff --git a/spec/requests/ci/builds_spec.rb b/spec/requests/ci/builds_spec.rb
deleted file mode 100644
index 998c386ead4..00000000000
--- a/spec/requests/ci/builds_spec.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'spec_helper'
-
-describe "Builds" do
- before do
- @project = FactoryGirl.create :ci_project
- @commit = FactoryGirl.create :ci_commit, project: @project
- @build = FactoryGirl.create :ci_build, commit: @commit
- end
-
- describe "GET /:project/builds/:id/status.json" do
- before do
- get status_ci_project_build_path(@project, @build), format: :json
- end
-
- it { expect(response.status).to eq(200) }
- it { expect(response.body).to include(@build.sha) }
- end
-end
diff --git a/spec/requests/ci/commits_spec.rb b/spec/requests/ci/commits_spec.rb
deleted file mode 100644
index fb317670339..00000000000
--- a/spec/requests/ci/commits_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require 'spec_helper'
-
-describe "Commits" do
- before do
- @project = FactoryGirl.create :ci_project
- @commit = FactoryGirl.create :ci_commit, project: @project
- end
-
- describe "GET /:project/refs/:ref_name/commits/:id/status.json" do
- before do
- get status_ci_project_ref_commits_path(@project, @commit.ref, @commit.sha), format: :json
- end
-
- it { expect(response.status).to eq(200) }
- it { expect(response.body).to include(@commit.sha) }
- end
-end
diff --git a/spec/services/archive_repository_service_spec.rb b/spec/services/archive_repository_service_spec.rb
index 0ec70c51b3a..f7a36cd9670 100644
--- a/spec/services/archive_repository_service_spec.rb
+++ b/spec/services/archive_repository_service_spec.rb
@@ -6,14 +6,14 @@ describe ArchiveRepositoryService do
describe "#execute" do
it "cleans old archives" do
- expect(project.repository).to receive(:clean_old_archives)
+ expect(RepositoryArchiveCacheWorker).to receive(:perform_async)
subject.execute(timeout: 0.0)
end
context "when the repository doesn't have an archive file path" do
before do
- allow(project.repository).to receive(:archive_file_path).and_return(nil)
+ allow(project.repository).to receive(:archive_metadata).and_return(Hash.new)
end
it "raises an error" do
@@ -21,70 +21,5 @@ describe ArchiveRepositoryService do
end
end
- context "when the repository has an archive file path" do
- let(:file_path) { "/archive.zip" }
- let(:pid_file_path) { "/archive.zip.pid" }
-
- before do
- allow(project.repository).to receive(:archive_file_path).and_return(file_path)
- allow(project.repository).to receive(:archive_pid_file_path).and_return(pid_file_path)
- end
-
- context "when the archive file already exists" do
- before do
- allow(File).to receive(:exist?).with(file_path).and_return(true)
- end
-
- it "returns the file path" do
- expect(subject.execute(timeout: 0.0)).to eq(file_path)
- end
- end
-
- context "when the archive file doesn't exist yet" do
- before do
- allow(File).to receive(:exist?).with(file_path).and_return(false)
- allow(File).to receive(:exist?).with(pid_file_path).and_return(true)
- end
-
- context "when the archive pid file doesn't exist yet" do
- before do
- allow(File).to receive(:exist?).with(pid_file_path).and_return(false)
- end
-
- it "queues the RepositoryArchiveWorker" do
- expect(RepositoryArchiveWorker).to receive(:perform_async)
-
- subject.execute(timeout: 0.0)
- end
- end
-
- context "when the archive pid file already exists" do
- it "doesn't queue the RepositoryArchiveWorker" do
- expect(RepositoryArchiveWorker).not_to receive(:perform_async)
-
- subject.execute(timeout: 0.0)
- end
- end
-
- context "when the archive file exists after a little while" do
- before do
- Thread.new do
- sleep 0.1
- allow(File).to receive(:exist?).with(file_path).and_return(true)
- end
- end
-
- it "returns the file path" do
- expect(subject.execute(timeout: 0.2)).to eq(file_path)
- end
- end
-
- context "when the archive file doesn't exist after the timeout" do
- it "returns nil" do
- expect(subject.execute(timeout: 0.0)).to eq(nil)
- end
- end
- end
- end
end
end
diff --git a/spec/services/ci/create_commit_service_spec.rb b/spec/services/ci/create_commit_service_spec.rb
index 84ab0a615dd..e0ede1d58b7 100644
--- a/spec/services/ci/create_commit_service_spec.rb
+++ b/spec/services/ci/create_commit_service_spec.rb
@@ -4,15 +4,19 @@ module Ci
describe CreateCommitService do
let(:service) { CreateCommitService.new }
let(:project) { FactoryGirl.create(:ci_project) }
+ let(:user) { nil }
+
+ before do
+ stub_ci_commit_to_return_yaml_file
+ end
describe :execute do
context 'valid params' do
let(:commit) do
- service.execute(project,
+ service.execute(project, user,
ref: 'refs/heads/master',
before: '00000000',
after: '31das312',
- ci_yaml_file: gitlab_ci_yaml,
commits: [ { message: "Message" } ]
)
end
@@ -26,11 +30,10 @@ module Ci
context "skip tag if there is no build for it" do
it "creates commit if there is appropriate job" do
- result = service.execute(project,
+ result = service.execute(project, user,
ref: 'refs/tags/0_1',
before: '00000000',
after: '31das312',
- ci_yaml_file: gitlab_ci_yaml,
commits: [ { message: "Message" } ]
)
expect(result).to be_persisted
@@ -38,103 +41,127 @@ module Ci
it "creates commit if there is no appropriate job but deploy job has right ref setting" do
config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } })
+ stub_ci_commit_yaml_file(config)
- result = service.execute(project,
+ result = service.execute(project, user,
ref: 'refs/heads/0_1',
before: '00000000',
after: '31das312',
- ci_yaml_file: config,
commits: [ { message: "Message" } ]
)
expect(result).to be_persisted
end
end
- it 'fails commits without .gitlab-ci.yml' do
- result = service.execute(project,
+ it 'skips commits without .gitlab-ci.yml' do
+ stub_ci_commit_yaml_file(nil)
+ result = service.execute(project, user,
ref: 'refs/heads/0_1',
before: '00000000',
after: '31das312',
- ci_yaml_file: config,
commits: [ { message: 'Message' } ]
)
expect(result).to be_persisted
expect(result.builds.any?).to be_falsey
- expect(result.status).to eq('failed')
+ expect(result.status).to eq('skipped')
+ expect(result.yaml_errors).to be_nil
+ end
+
+ it 'skips commits if yaml is invalid' do
+ message = 'message'
+ allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message }
+ stub_ci_commit_yaml_file('invalid: file: file')
+ commits = [{ message: message }]
+ commit = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(commit.builds.any?).to be false
+ expect(commit.status).to eq('failed')
+ expect(commit.yaml_errors).to_not be_nil
end
describe :ci_skip? do
+ let(:message) { "some message[ci skip]" }
+
+ before do
+ allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message }
+ end
+
it "skips builds creation if there is [ci skip] tag in commit message" do
- commits = [{ message: "some message[ci skip]" }]
- commit = service.execute(project,
+ commits = [{ message: message }]
+ commit = service.execute(project, user,
ref: 'refs/tags/0_1',
before: '00000000',
after: '31das312',
- commits: commits,
- ci_yaml_file: gitlab_ci_yaml
+ commits: commits
)
expect(commit.builds.any?).to be false
expect(commit.status).to eq("skipped")
end
it "does not skips builds creation if there is no [ci skip] tag in commit message" do
- commits = [{ message: "some message" }]
+ allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { "some message" }
- commit = service.execute(project,
+ commits = [{ message: "some message" }]
+ commit = service.execute(project, user,
ref: 'refs/tags/0_1',
before: '00000000',
after: '31das312',
- commits: commits,
- ci_yaml_file: gitlab_ci_yaml
+ commits: commits
)
expect(commit.builds.first.name).to eq("staging")
end
it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
- commits = [{ message: "some message[ci skip]" }]
- commit = service.execute(project,
+ stub_ci_commit_yaml_file('invalid: file: fiile')
+ commits = [{ message: message }]
+ commit = service.execute(project, user,
ref: 'refs/tags/0_1',
before: '00000000',
after: '31das312',
- commits: commits,
- ci_yaml_file: "invalid: file"
+ commits: commits
)
expect(commit.builds.any?).to be false
expect(commit.status).to eq("skipped")
+ expect(commit.yaml_errors).to be_nil
end
end
it "skips build creation if there are already builds" do
+ allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { gitlab_ci_yaml }
+
commits = [{ message: "message" }]
- commit = service.execute(project,
+ commit = service.execute(project, user,
ref: 'refs/heads/master',
before: '00000000',
after: '31das312',
- commits: commits,
- ci_yaml_file: gitlab_ci_yaml
+ commits: commits
)
expect(commit.builds.count(:all)).to eq(2)
- commit = service.execute(project,
+ commit = service.execute(project, user,
ref: 'refs/heads/master',
before: '00000000',
after: '31das312',
- commits: commits,
- ci_yaml_file: gitlab_ci_yaml
+ commits: commits
)
expect(commit.builds.count(:all)).to eq(2)
end
it "creates commit with failed status if yaml is invalid" do
+ stub_ci_commit_yaml_file('invalid: file')
+
commits = [{ message: "some message" }]
- commit = service.execute(project,
+ commit = service.execute(project, user,
ref: 'refs/tags/0_1',
before: '00000000',
after: '31das312',
- commits: commits,
- ci_yaml_file: "invalid: file"
+ commits: commits
)
expect(commit.status).to eq("failed")
diff --git a/spec/services/ci/create_project_service_spec.rb b/spec/services/ci/create_project_service_spec.rb
deleted file mode 100644
index 2de7b0deca7..00000000000
--- a/spec/services/ci/create_project_service_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'spec_helper'
-
-describe Ci::CreateProjectService do
- let(:service) { Ci::CreateProjectService.new }
- let(:current_user) { double.as_null_object }
- let(:project) { FactoryGirl.create :project }
-
- describe :execute do
- context 'valid params' do
- subject { service.execute(current_user, project) }
-
- it { is_expected.to be_kind_of(Ci::Project) }
- it { is_expected.to be_persisted }
- end
-
- context 'without project dump' do
- it 'should raise exception' do
- expect { service.execute(current_user, '', '') }.
- to raise_error(NoMethodError)
- end
- end
-
- context "forking" do
- let(:ci_origin_project) do
- FactoryGirl.create(:ci_project, shared_runners_enabled: true, public: true, allow_git_fetch: true)
- end
-
- subject { service.execute(current_user, project, ci_origin_project) }
-
- it "uses project as a template for settings and jobs" do
- expect(subject.shared_runners_enabled).to be_truthy
- expect(subject.public).to be_truthy
- expect(subject.allow_git_fetch).to be_truthy
- end
- end
- end
-end
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index d12cd9773dc..2ef4bb50a57 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -2,19 +2,20 @@ require 'spec_helper'
describe Ci::CreateTriggerRequestService do
let(:service) { Ci::CreateTriggerRequestService.new }
- let(:project) { FactoryGirl.create :ci_project }
- let(:trigger) { FactoryGirl.create :ci_trigger, project: project }
+ let(:gl_project) { create(:project) }
+ let(:project) { gl_project.ensure_gitlab_ci_project }
+ let(:trigger) { create(:ci_trigger, project: project) }
+
+ before do
+ stub_ci_commit_to_return_yaml_file
+ end
describe :execute do
context 'valid params' do
subject { service.execute(project, trigger, 'master') }
- before do
- @commit = FactoryGirl.create :ci_commit, project: project
- end
-
it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
- it { expect(subject.commit).to eq(@commit) }
+ it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
end
context 'no commit for ref' do
@@ -27,26 +28,11 @@ describe Ci::CreateTriggerRequestService do
subject { service.execute(project, trigger, 'master') }
before do
- FactoryGirl.create :ci_commit_without_jobs, project: project
+ stub_ci_commit_yaml_file('{}')
+ FactoryGirl.create :ci_commit, gl_project: gl_project
end
it { expect(subject).to be_nil }
end
-
- context 'for multiple commits' do
- subject { service.execute(project, trigger, 'master') }
-
- before do
- @commit1 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project
- @commit2 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
- @commit3 = FactoryGirl.create :ci_commit, committed_at: 3.hour.ago, project: project
- end
-
- context 'retries latest one' do
- it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
- it { expect(subject).to be_persisted }
- it { expect(subject.commit).to eq(@commit2) }
- end
- end
end
end
diff --git a/spec/services/ci/event_service_spec.rb b/spec/services/ci/event_service_spec.rb
index 9b330a90ae2..1264e17ff5e 100644
--- a/spec/services/ci/event_service_spec.rb
+++ b/spec/services/ci/event_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::EventService do
- let(:project) { FactoryGirl.create :ci_project, name: "GitLab / gitlab-shell" }
+ let(:project) { FactoryGirl.create :ci_project }
let(:user) { double(username: "root", id: 1) }
before do
@@ -12,7 +12,7 @@ describe Ci::EventService do
it "creates event" do
Ci::EventService.new.remove_project(user, project)
- expect(Ci::Event.admin.last.description).to eq("Project \"GitLab / gitlab-shell\" has been removed by root")
+ expect(Ci::Event.admin.last.description).to eq("Project \"#{project.name_with_namespace}\" has been removed by root")
end
end
@@ -20,7 +20,7 @@ describe Ci::EventService do
it "creates event" do
Ci::EventService.new.create_project(user, project)
- expect(Ci::Event.admin.last.description).to eq("Project \"GitLab / gitlab-shell\" has been created by root")
+ expect(Ci::Event.admin.last.description).to eq("Project \"#{project.name_with_namespace}\" has been created by root")
end
end
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
index 7565eb8f032..cda7d0c4a51 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -4,7 +4,9 @@ module Ci
describe ImageForBuildService do
let(:service) { ImageForBuildService.new }
let(:project) { FactoryGirl.create(:ci_project) }
- let(:commit) { FactoryGirl.create(:ci_commit, project: project, ref: 'master') }
+ let(:gl_project) { FactoryGirl.create(:project, gitlab_ci_project: project) }
+ let(:commit_sha) { gl_project.commit('master').sha }
+ let(:commit) { gl_project.ensure_ci_commit(commit_sha) }
let(:build) { FactoryGirl.create(:ci_build, commit: commit) }
describe :execute do
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
index 7b5af6c3dd0..b370dfbe113 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -3,14 +3,14 @@ require 'spec_helper'
module Ci
describe RegisterBuildService do
let!(:service) { RegisterBuildService.new }
- let!(:project) { FactoryGirl.create :ci_project }
- let!(:commit) { FactoryGirl.create :ci_commit, project: project }
- let!(:pending_build) { FactoryGirl.create :ci_build, project: project, commit: commit }
+ let!(:gl_project) { FactoryGirl.create :empty_project }
+ let!(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+ let!(:pending_build) { FactoryGirl.create :ci_build, commit: commit }
let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) }
let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) }
before do
- specific_runner.assign_to(project)
+ specific_runner.assign_to(gl_project.ensure_gitlab_ci_project)
end
describe :execute do
@@ -47,8 +47,7 @@ module Ci
context 'allow shared runners' do
before do
- project.shared_runners_enabled = true
- project.save
+ gl_project.gitlab_ci_project.update(shared_runners_enabled: true)
end
context 'shared runner' do
@@ -71,6 +70,10 @@ module Ci
end
context 'disallow shared runners' do
+ before do
+ gl_project.gitlab_ci_project.update(shared_runners_enabled: false)
+ end
+
context 'shared runner' do
let(:build) { service.execute(shared_runner) }
diff --git a/spec/services/ci/web_hook_service_spec.rb b/spec/services/ci/web_hook_service_spec.rb
index cebdd145e40..aa48fcbcbfd 100644
--- a/spec/services/ci/web_hook_service_spec.rb
+++ b/spec/services/ci/web_hook_service_spec.rb
@@ -2,7 +2,8 @@ require 'spec_helper'
describe Ci::WebHookService do
let(:project) { FactoryGirl.create :ci_project }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
+ let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
let(:build) { FactoryGirl.create :ci_build, commit: commit }
let(:hook) { FactoryGirl.create :ci_web_hook, project: project }
diff --git a/spec/services/create_release_service_spec.rb b/spec/services/create_release_service_spec.rb
new file mode 100644
index 00000000000..26d7f365bbb
--- /dev/null
+++ b/spec/services/create_release_service_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe CreateReleaseService do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:tag_name) { project.repository.tag_names.first }
+ let(:description) { 'Awesome release!' }
+ let(:service) { CreateReleaseService.new(project, user) }
+
+ it 'creates a new release' do
+ result = service.execute(tag_name, description)
+ expect(result[:status]).to eq(:success)
+ release = project.releases.find_by(tag: tag_name)
+ expect(release).not_to be_nil
+ expect(release.description).to eq(description)
+ end
+
+ it 'raises an error if the tag does not exist' do
+ result = service.execute("foobar", description)
+ expect(result[:status]).to eq(:error)
+ end
+
+ context 'there already exists a release on a tag' do
+ before do
+ service.execute(tag_name, description)
+ end
+
+ it 'raises an error and does not update the release' do
+ result = service.execute(tag_name, 'The best release!')
+ expect(result[:status]).to eq(:error)
+ expect(project.releases.find_by(tag: tag_name).description).to eq(description)
+ end
+ end
+end
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
new file mode 100644
index 00000000000..7e018d3c9fe
--- /dev/null
+++ b/spec/services/git_hooks_service_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe GitHooksService do
+ include RepoHelpers
+
+ let(:user) { create :user }
+ let(:project) { create :project }
+ let(:service) { GitHooksService.new }
+
+ before do
+ @blankrev = Gitlab::Git::BLANK_SHA
+ @oldrev = sample_commit.parent_id
+ @newrev = sample_commit.id
+ @ref = 'refs/heads/feature'
+ @repo_path = project.repository.path_to_repo
+ end
+
+ describe '#execute' do
+
+ context 'when receive hooks were successful' do
+ it 'should call post-receive hook' do
+ hook = double(trigger: true)
+ expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
+
+ expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq(true)
+ end
+ end
+
+ context 'when pre-receive hook failed' do
+ it 'should not call post-receive hook' do
+ expect(service).to receive(:run_hook).with('pre-receive').and_return(false)
+ expect(service).not_to receive(:run_hook).with('post-receive')
+
+ expect do
+ service.execute(user, @repo_path, @blankrev, @newrev, @ref)
+ end.to raise_error(GitHooksService::PreReceiveError)
+ end
+ end
+
+ context 'when update hook failed' do
+ it 'should not call post-receive hook' do
+ expect(service).to receive(:run_hook).with('pre-receive').and_return(true)
+ expect(service).to receive(:run_hook).with('update').and_return(false)
+ expect(service).not_to receive(:run_hook).with('post-receive')
+
+ expect do
+ service.execute(user, @repo_path, @blankrev, @newrev, @ref)
+ end.to raise_error(GitHooksService::PreReceiveError)
+ end
+ end
+
+ end
+end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index c483060fd73..17015d29e51 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -112,6 +112,14 @@ describe GitPushService do
it { expect(@event.project).to eq(project) }
it { expect(@event.action).to eq(Event::PUSHED) }
it { expect(@event.data).to eq(service.push_data) }
+
+ context "Updates merge requests" do
+ it "when pushing a new branch for the first time" do
+ expect(project).to receive(:update_merge_requests).
+ with(@blankrev, 'newrev', 'refs/heads/master', user)
+ service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master')
+ end
+ end
end
describe "Web Hooks" do
@@ -155,7 +163,7 @@ describe GitPushService do
before do
allow(commit).to receive_messages(
- safe_message: "this commit \n mentions ##{issue.id}",
+ safe_message: "this commit \n mentions #{issue.to_reference}",
references: [issue],
author_name: commit_author.name,
author_email: commit_author.email
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index db547ce0d50..d711e3da104 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -14,7 +14,9 @@ describe Issues::CloseService do
describe :execute do
context "valid params" do
before do
- @issue = Issues::CloseService.new(project, user, {}).execute(issue)
+ perform_enqueued_jobs do
+ @issue = Issues::CloseService.new(project, user, {}).execute(issue)
+ end
end
it { expect(@issue).to be_valid }
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index a91be3b4472..73d0b7f7abe 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -3,16 +3,29 @@ require 'spec_helper'
describe Issues::UpdateService do
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let(:issue) { create(:issue, title: 'Old title') }
+ let(:user3) { create(:user) }
+ let(:issue) { create(:issue, title: 'Old title', assignee_id: user3.id) }
let(:label) { create(:label) }
let(:project) { issue.project }
before do
project.team << [user, :master]
project.team << [user2, :developer]
+ project.team << [user3, :developer]
end
describe 'execute' do
+ def find_note(starting_with)
+ @issue.notes.find do |note|
+ note && note.note.start_with?(starting_with)
+ end
+ end
+
+ def update_issue(opts)
+ @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ @issue.reload
+ end
+
context "valid params" do
before do
opts = {
@@ -23,7 +36,10 @@ describe Issues::UpdateService do
label_ids: [label.id]
}
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ perform_enqueued_jobs do
+ @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ end
+
@issue.reload
end
@@ -34,18 +50,14 @@ describe Issues::UpdateService do
it { expect(@issue.labels.count).to eq(1) }
it { expect(@issue.labels.first.title).to eq('Bug') }
- it 'should send email to user2 about assign of new issue' do
- email = ActionMailer::Base.deliveries.last
- expect(email.to.first).to eq(user2.email)
+ it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do
+ deliveries = ActionMailer::Base.deliveries
+ email = deliveries.last
+ recipients = deliveries.last(2).map(&:to).flatten
+ expect(recipients).to include(user2.email, user3.email)
expect(email.subject).to include(issue.title)
end
- def find_note(starting_with)
- @issue.notes.find do |note|
- note && note.note.start_with?(starting_with)
- end
- end
-
it 'should create system note about issue reassign' do
note = find_note('Reassigned to')
@@ -67,5 +79,71 @@ describe Issues::UpdateService do
expect(note.note).to eq 'Title changed from **Old title** to **New title**'
end
end
+
+ context 'when Issue has tasks' do
+ before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
+
+ it { expect(@issue.tasks?).to eq(true) }
+
+ context 'when tasks are marked as completed' do
+ before { update_issue({ description: "- [x] Task 1\n- [X] Task 2" }) }
+
+ it 'creates system note about task status change' do
+ note1 = find_note('Marked the task **Task 1** as completed')
+ note2 = find_note('Marked the task **Task 2** as completed')
+
+ expect(note1).not_to be_nil
+ expect(note2).not_to be_nil
+ end
+ end
+
+ context 'when tasks are marked as incomplete' do
+ before do
+ update_issue({ description: "- [x] Task 1\n- [X] Task 2" })
+ update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" })
+ end
+
+ it 'creates system note about task status change' do
+ note1 = find_note('Marked the task **Task 1** as incomplete')
+ note2 = find_note('Marked the task **Task 2** as incomplete')
+
+ expect(note1).not_to be_nil
+ expect(note2).not_to be_nil
+ end
+ end
+
+ context 'when tasks position has been modified' do
+ before do
+ update_issue({ description: "- [x] Task 1\n- [X] Task 2" })
+ update_issue({ description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2" })
+ end
+
+ it 'does not create a system note' do
+ note = find_note('Marked the task **Task 2** as incomplete')
+
+ expect(note).to be_nil
+ end
+ end
+
+ context 'when a Task list with a completed item is totally replaced' do
+ before do
+ update_issue({ description: "- [ ] Task 1\n- [X] Task 2" })
+ update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
+ end
+
+ it 'does not create a system note referencing the position the old item' do
+ note = find_note('Marked the task **Two** as incomplete')
+
+ expect(note).to be_nil
+ end
+
+ it 'should not generate a new note at all' do
+ expect do
+ update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
+ end.not_to change { Note.count }
+ end
+ end
+ end
+
end
end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index b3cbfd4b5b8..272f3897938 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -18,7 +18,9 @@ describe MergeRequests::CloseService do
before do
allow(service).to receive(:execute_hooks)
- @merge_request = service.execute(merge_request)
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ end
end
it { expect(@merge_request).to be_valid }
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 7b564d34d7b..c0961ceb11e 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -17,8 +17,9 @@ describe MergeRequests::MergeService do
before do
allow(service).to receive(:execute_hooks)
-
- service.execute(merge_request, 'Awesome message')
+ perform_enqueued_jobs do
+ service.execute(merge_request, 'Awesome message')
+ end
end
it { expect(merge_request).to be_valid }
@@ -35,5 +36,19 @@ describe MergeRequests::MergeService do
expect(note.note).to include 'Status changed to merged'
end
end
+
+ context "error handling" do
+ let(:service) { MergeRequests::MergeService.new(project, user, {}) }
+
+ it 'saves error if there is an exception' do
+ allow(service).to receive(:repository).and_raise("error")
+
+ allow(service).to receive(:execute_hooks)
+
+ service.execute(merge_request, 'Awesome message')
+
+ expect(merge_request.merge_error).to eq("Something went wrong during merge")
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 9516e7936d8..7ee4488521d 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -62,6 +62,25 @@ describe MergeRequests::RefreshService do
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
end
+ context 'manual merge of source branch' do
+ before do
+ # Merge master -> feature branch
+ author = { email: 'test@gitlab.com', time: Time.now, name: "Me" }
+ commit_options = { message: 'Test message', committer: author, author: author }
+ master_commit = @project.repository.commit('master')
+ @project.repository.merge(@user, master_commit.id, 'feature', commit_options)
+ commit = @project.repository.commit('feature')
+ service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature')
+ reload_mrs
+ end
+
+ it { expect(@merge_request.notes.last.note).to include('changed to merged') }
+ it { expect(@merge_request).to be_merged }
+ it { expect(@merge_request.diffs.length).to be > 0 }
+ it { expect(@fork_merge_request).to be_merged }
+ it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
+ end
+
context 'push to fork repo source branch' do
let(:refresh_service) { service.new(@fork_project, @user) }
before do
@@ -106,6 +125,27 @@ describe MergeRequests::RefreshService do
it { expect(@fork_merge_request.notes).to be_empty }
end
+ context 'push new branch that exists in a merge request' do
+ let(:refresh_service) { service.new(@fork_project, @user) }
+
+ it 'refreshes the merge request' do
+ expect(refresh_service).to receive(:execute_hooks).
+ with(@fork_merge_request, 'update')
+ allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev)
+
+ refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master')
+ reload_mrs
+
+ expect(@merge_request.notes).to be_empty
+ expect(@merge_request).to be_open
+
+ notes = @fork_merge_request.notes.reorder(:created_at).map(&:note)
+ expect(notes[0]).to include('Restored source branch `master`')
+ expect(notes[1]).to include('Added 4 commits')
+ expect(@fork_merge_request).to be_open
+ end
+ end
+
def reload_mrs
@merge_request.reload
@fork_merge_request.reload
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index 9401bc3b558..05146bf43f4 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -19,7 +19,9 @@ describe MergeRequests::ReopenService do
allow(service).to receive(:execute_hooks)
merge_request.state = :closed
- service.execute(merge_request)
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ end
end
it { expect(merge_request).to be_valid }
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index c75173c1452..d899b1f01d1 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -3,7 +3,8 @@ require 'spec_helper'
describe MergeRequests::UpdateService do
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let(:merge_request) { create(:merge_request, :simple, title: 'Old title') }
+ let(:user3) { create(:user) }
+ let(:merge_request) { create(:merge_request, :simple, title: 'Old title', assignee_id: user3.id) }
let(:project) { merge_request.project }
let(:label) { create(:label) }
@@ -13,6 +14,17 @@ describe MergeRequests::UpdateService do
end
describe 'execute' do
+ def find_note(starting_with)
+ @merge_request.notes.find do |note|
+ note && note.note.start_with?(starting_with)
+ end
+ end
+
+ def update_merge_request(opts)
+ @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
+ @merge_request.reload
+ end
+
context 'valid params' do
let(:opts) do
{
@@ -30,8 +42,10 @@ describe MergeRequests::UpdateService do
before do
allow(service).to receive(:execute_hooks)
- @merge_request = service.execute(merge_request)
- @merge_request.reload
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ @merge_request.reload
+ end
end
it { expect(@merge_request).to be_valid }
@@ -47,18 +61,14 @@ describe MergeRequests::UpdateService do
with(@merge_request, 'update')
end
- it 'should send email to user2 about assign of new merge_request' do
- email = ActionMailer::Base.deliveries.last
- expect(email.to.first).to eq(user2.email)
+ it 'should send email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
+ deliveries = ActionMailer::Base.deliveries
+ email = deliveries.last
+ recipients = deliveries.last(2).map(&:to).flatten
+ expect(recipients).to include(user2.email, user3.email)
expect(email.subject).to include(merge_request.title)
end
- def find_note(starting_with)
- @merge_request.notes.find do |note|
- note && note.note.start_with?(starting_with)
- end
- end
-
it 'should create system note about merge_request reassign' do
note = find_note('Reassigned to')
@@ -87,5 +97,39 @@ describe MergeRequests::UpdateService do
expect(note.note).to eq 'Target branch changed from `master` to `target`'
end
end
+
+ context 'when MergeRequest has tasks' do
+ before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
+
+ it { expect(@merge_request.tasks?).to eq(true) }
+
+ context 'when tasks are marked as completed' do
+ before { update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" }) }
+
+ it 'creates system note about task status change' do
+ note1 = find_note('Marked the task **Task 1** as completed')
+ note2 = find_note('Marked the task **Task 2** as completed')
+
+ expect(note1).not_to be_nil
+ expect(note2).not_to be_nil
+ end
+ end
+
+ context 'when tasks are marked as incomplete' do
+ before do
+ update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" })
+ update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" })
+ end
+
+ it 'creates system note about task status change' do
+ note1 = find_note('Marked the task **Task 1** as incomplete')
+ note2 = find_note('Marked the task **Task 2** as incomplete')
+
+ expect(note1).not_to be_nil
+ expect(note2).not_to be_nil
+ end
+ end
+ end
+
end
end
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
new file mode 100644
index 00000000000..034c0f22e12
--- /dev/null
+++ b/spec/services/milestones/close_service_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Milestones::CloseService do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe :execute do
+ before do
+ Milestones::CloseService.new(project, user, {}).execute(milestone)
+ end
+
+ it { expect(milestone).to be_valid }
+ it { expect(milestone).to be_closed }
+
+ describe :event do
+ let(:event) { Event.first }
+
+ it { expect(event.milestone).to be_truthy }
+ it { expect(event.target).to eq(milestone) }
+ it { expect(event.action_name).to eq('closed') }
+ end
+ end
+end
diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb
new file mode 100644
index 00000000000..757c9a226d8
--- /dev/null
+++ b/spec/services/milestones/create_service_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Milestones::CreateService do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ describe :execute do
+ context "valid params" do
+ before do
+ project.team << [user, :master]
+
+ opts = {
+ title: 'v2.1.9',
+ description: 'Patch release to fix security issue'
+ }
+
+ @milestone = Milestones::CreateService.new(project, user, opts).execute
+ end
+
+ it { expect(@milestone).to be_valid }
+ it { expect(@milestone.title).to eq('v2.1.9') }
+ end
+ end
+end
diff --git a/spec/services/milestones/group_service_spec.rb b/spec/services/milestones/group_service_spec.rb
deleted file mode 100644
index 74eb0f99e0f..00000000000
--- a/spec/services/milestones/group_service_spec.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-require 'spec_helper'
-
-describe Milestones::GroupService do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:group) { create(:group) }
- let(:project1) { create(:project, group: group) }
- let(:project2) { create(:project, path: 'gitlab-ci', group: group) }
- let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) }
- let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
- let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
- let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) }
- let(:milestone2_project1) { create(:milestone, title: "VD-123", project: project1) }
- let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) }
- let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) }
-
- describe 'execute' do
- context 'with valid projects' do
- before do
- milestones =
- [
- milestone1_project1,
- milestone1_project2,
- milestone1_project3,
- milestone2_project1,
- milestone2_project2,
- milestone2_project3
- ]
- @group_milestones = Milestones::GroupService.new(milestones).execute
- end
-
- it 'should have all project milestones' do
- expect(@group_milestones.count).to eq(2)
- end
-
- it 'should have all project milestones titles' do
- expect(@group_milestones.map { |group_milestone| group_milestone.title }).to match_array(['Milestone v1.2', 'VD-123'])
- end
-
- it 'should have all project milestones' do
- expect(@group_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
- end
- end
- end
-
- describe 'milestone' do
- context 'with valid title' do
- before do
- milestones =
- [
- milestone1_project1,
- milestone1_project2,
- milestone1_project3,
- milestone2_project1,
- milestone2_project2,
- milestone2_project3
- ]
- @group_milestones = Milestones::GroupService.new(milestones).milestone('Milestone v1.2')
- end
-
- it 'should have exactly one group milestone' do
- expect(@group_milestones.title).to eq('Milestone v1.2')
- end
-
- it 'should have all project milestones with the same title' do
- expect(@group_milestones.milestones.count).to eq(3)
- end
- end
- end
-end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index f2ea0805b2f..cc38d257792 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -24,4 +24,38 @@ describe Notes::CreateService do
it { expect(@note.note).to eq('Awesome comment') }
end
end
+
+ describe "award emoji" do
+ before do
+ project.team << [user, :master]
+ end
+
+ it "creates emoji note" do
+ opts = {
+ note: ':smile: ',
+ noteable_type: 'Issue',
+ noteable_id: issue.id
+ }
+
+ @note = Notes::CreateService.new(project, user, opts).execute
+
+ expect(@note).to be_valid
+ expect(@note.note).to eq('smile')
+ expect(@note.is_award).to be_truthy
+ end
+
+ it "creates regular note if emoji name is invalid" do
+ opts = {
+ note: ':smile: moretext: ',
+ noteable_type: 'Issue',
+ noteable_id: issue.id
+ }
+
+ @note = Notes::CreateService.new(project, user, opts).execute
+
+ expect(@note).to be_valid
+ expect(@note.note).to eq(opts[:note])
+ expect(@note.is_award).to be_falsy
+ end
+ end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 8865335d0d1..35fa412ed80 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -3,6 +3,12 @@ require 'spec_helper'
describe NotificationService do
let(:notification) { NotificationService.new }
+ around(:each) do |example|
+ perform_enqueued_jobs do
+ example.run
+ end
+ end
+
describe 'Keys' do
describe :new_key do
let!(:key) { create(:personal_key) }
@@ -10,8 +16,7 @@ describe NotificationService do
it { expect(notification.new_key(key)).to be_truthy }
it 'should sent email to key owner' do
- expect(Notify).to receive(:new_ssh_key_email).with(key.id)
- notification.new_key(key)
+ expect{ notification.new_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
end
end
end
@@ -23,8 +28,7 @@ describe NotificationService do
it { expect(notification.new_email(email)).to be_truthy }
it 'should send email to email owner' do
- expect(Notify).to receive(:new_email_email).with(email.id)
- notification.new_email(email)
+ expect{ notification.new_email(email) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
end
end
end
@@ -41,24 +45,28 @@ describe NotificationService do
project.team << [issue.author, :master]
project.team << [issue.assignee, :master]
project.team << [note.author, :master]
+ create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy')
end
describe :new_note do
it do
add_users_with_subscription(note.project, issue)
- should_email(@u_watcher.id)
- should_email(note.noteable.author_id)
- should_email(note.noteable.assignee_id)
- should_email(@u_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(note.author_id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_outsider_mentioned)
+ ActionMailer::Base.deliveries.clear
notification.new_note(note)
+
+ should_email(@u_watcher)
+ should_email(note.noteable.author)
+ should_email(note.noteable.assignee)
+ should_email(@u_mentioned)
+ should_email(@subscriber)
+ should_email(@subscribed_participant)
+ should_not_email(note.author)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_outsider_mentioned)
end
it 'filters out "mentioned in" notes' do
@@ -82,26 +90,20 @@ describe NotificationService do
group_member = note.project.group.group_members.find_by_user_id(@u_watcher.id)
group_member.notification_level = Notification::N_GLOBAL
group_member.save
+ ActionMailer::Base.deliveries.clear
end
it do
- should_email(note.noteable.author_id)
- should_email(note.noteable.assignee_id)
- should_email(@u_mentioned.id)
- should_not_email(@u_watcher.id)
- should_not_email(note.author_id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.new_note(note)
- end
- end
- def should_email(user_id)
- expect(Notify).to receive(:note_issue_email).with(user_id, note.id)
- end
-
- def should_not_email(user_id)
- expect(Notify).not_to receive(:note_issue_email).with(user_id, note.id)
+ should_email(note.noteable.author)
+ should_email(note.noteable.assignee)
+ should_email(@u_mentioned)
+ should_not_email(@u_watcher)
+ should_not_email(note.author)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ end
end
end
@@ -113,24 +115,26 @@ describe NotificationService do
before do
build_team(note.project)
+ ActionMailer::Base.deliveries.clear
end
describe :new_note do
it do
+ notification.new_note(note)
+
# Notify all team members
note.project.team.members.each do |member|
# User with disabled notification should not be notified
next if member.id == @u_disabled.id
- should_email(member.id)
+ should_email(member)
end
- should_email(note.noteable.author_id)
- should_email(note.noteable.assignee_id)
- should_not_email(note.author_id)
- should_not_email(@u_mentioned.id)
- should_not_email(@u_disabled.id)
- should_not_email(@u_not_mentioned.id)
- notification.new_note(note)
+ should_email(note.noteable.author)
+ should_email(note.noteable.assignee)
+ should_not_email(note.author)
+ should_email(@u_mentioned)
+ should_not_email(@u_disabled)
+ should_email(@u_not_mentioned)
end
it 'filters out "mentioned in" notes' do
@@ -140,14 +144,6 @@ describe NotificationService do
notification.new_note(mentioned_note)
end
end
-
- def should_email(user_id)
- expect(Notify).to receive(:note_issue_email).with(user_id, note.id)
- end
-
- def should_not_email(user_id)
- expect(Notify).not_to receive(:note_issue_email).with(user_id, note.id)
- end
end
context 'commit note' do
@@ -156,43 +152,38 @@ describe NotificationService do
before do
build_team(note.project)
+ ActionMailer::Base.deliveries.clear
allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer)
end
- describe :new_note do
+ describe :new_note, :perform_enqueued_jobs do
it do
- should_email(@u_committer.id, note)
- should_email(@u_watcher.id, note)
- should_not_email(@u_mentioned.id, note)
- should_not_email(note.author_id, note)
- should_not_email(@u_participating.id, note)
- should_not_email(@u_disabled.id, note)
notification.new_note(note)
+
+ should_email(@u_committer)
+ should_email(@u_watcher)
+ should_not_email(@u_mentioned)
+ should_not_email(note.author)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
it do
note.update_attribute(:note, '@mention referenced')
- should_email(@u_committer.id, note)
- should_email(@u_watcher.id, note)
- should_email(@u_mentioned.id, note)
- should_not_email(note.author_id, note)
- should_not_email(@u_participating.id, note)
- should_not_email(@u_disabled.id, note)
notification.new_note(note)
+
+ should_email(@u_committer)
+ should_email(@u_watcher)
+ should_email(@u_mentioned)
+ should_not_email(note.author)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
it do
@u_committer.update_attributes(notification_level: Notification::N_MENTION)
- should_not_email(@u_committer.id, note)
notification.new_note(note)
- end
-
- def should_email(user_id, n)
- expect(Notify).to receive(:note_commit_email).with(user_id, n.id)
- end
-
- def should_not_email(user_id, n)
- expect(Notify).not_to receive(:note_commit_email).with(user_id, n.id)
+ should_not_email(@u_committer)
end
end
end
@@ -205,99 +196,69 @@ describe NotificationService do
before do
build_team(issue.project)
add_users_with_subscription(issue.project, issue)
+ ActionMailer::Base.deliveries.clear
end
describe :new_issue do
it do
- should_email(issue.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_not_email(@u_mentioned.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.new_issue(issue, @u_disabled)
+
+ should_email(issue.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_not_email(@u_mentioned)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
it do
issue.assignee.update_attributes(notification_level: Notification::N_MENTION)
- should_not_email(issue.assignee_id)
notification.new_issue(issue, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:new_issue_email).with(user_id, issue.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:new_issue_email).with(user_id, issue.id)
+ should_not_email(issue.assignee)
end
end
describe :reassigned_issue do
it 'should email new assignee' do
- should_email(issue.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
-
notification.reassigned_issue(issue, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:reassigned_issue_email).with(user_id, issue.id, nil, @u_disabled.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:reassigned_issue_email).with(user_id, issue.id, issue.assignee_id, @u_disabled.id)
+ should_email(issue.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
describe :close_issue do
it 'should sent email to issue assignee and issue author' do
- should_email(issue.assignee_id)
- should_email(issue.author_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
-
notification.close_issue(issue, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id)
+ should_email(issue.assignee)
+ should_email(issue.author)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
describe :reopen_issue do
it 'should send email to issue assignee and issue author' do
- should_email(issue.assignee_id)
- should_email(issue.author_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
-
notification.reopen_issue(issue, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id)
+ should_email(issue.assignee)
+ should_email(issue.author)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
end
end
end
@@ -309,108 +270,74 @@ describe NotificationService do
before do
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
+ ActionMailer::Base.deliveries.clear
end
describe :new_merge_request do
it do
- should_email(merge_request.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.new_merge_request(merge_request, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:new_merge_request_email).with(user_id, merge_request.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:new_merge_request_email).with(user_id, merge_request.id)
+ should_email(merge_request.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
describe :reassigned_merge_request do
it do
- should_email(merge_request.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.reassigned_merge_request(merge_request, merge_request.author)
- end
- def should_email(user_id)
- expect(Notify).to receive(:reassigned_merge_request_email).with(user_id, merge_request.id, nil, merge_request.author_id)
- end
-
- def should_not_email(user_id)
- expect(Notify).not_to receive(:reassigned_merge_request_email).with(user_id, merge_request.id, merge_request.assignee_id, merge_request.author_id)
+ should_email(merge_request.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
describe :closed_merge_request do
it do
- should_email(merge_request.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.close_mr(merge_request, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id)
+ should_email(merge_request.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
describe :merged_merge_request do
it do
- should_email(merge_request.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.merge_mr(merge_request, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id)
+ should_email(merge_request.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
describe :reopen_merge_request do
it do
- should_email(merge_request.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.reopen_mr(merge_request, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id)
+ should_email(merge_request.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
end
@@ -420,22 +347,16 @@ describe NotificationService do
before do
build_team(project)
+ ActionMailer::Base.deliveries.clear
end
describe :project_was_moved do
it do
- should_email(@u_watcher.id)
- should_email(@u_participating.id)
- should_not_email(@u_disabled.id)
- notification.project_was_moved(project)
- end
+ notification.project_was_moved(project, "gitlab/gitlab")
- def should_email(user_id)
- expect(Notify).to receive(:project_was_moved_email).with(project.id, user_id)
- end
-
- def should_not_email(user_id)
- expect(Notify).not_to receive(:project_was_moved_email).with(project.id, user_id)
+ should_email(@u_watcher)
+ should_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
end
@@ -462,11 +383,26 @@ describe NotificationService do
def add_users_with_subscription(project, issuable)
@subscriber = create :user
@unsubscriber = create :user
+ @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: Notification::N_PARTICIPATING)
+ project.team << [@subscribed_participant, :master]
project.team << [@subscriber, :master]
project.team << [@unsubscriber, :master]
issuable.subscriptions.create(user: @subscriber, subscribed: true)
+ issuable.subscriptions.create(user: @subscribed_participant, subscribed: true)
issuable.subscriptions.create(user: @unsubscriber, subscribed: false)
end
+
+ def sent_to_user?(user)
+ ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
+ end
+
+ def should_email(user)
+ expect(sent_to_user?(user)).to be_truthy
+ end
+
+ def should_not_email(user)
+ expect(sent_to_user?(user)).to be_falsey
+ end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 25277f07482..e81c4edb7d8 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -70,6 +70,28 @@ describe Projects::CreateService do
end
end
+ context 'builds_enabled global setting' do
+ let(:project) { create_project(@user, @opts) }
+
+ subject { project.builds_enabled? }
+
+ context 'global builds_enabled false does not enable CI by default' do
+ before do
+ @opts.merge!(builds_enabled: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'global builds_enabled true does enable CI by default' do
+ before do
+ @opts.merge!(builds_enabled: true)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
context 'restricted visibility level' do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
index f12e09c58c3..ddee2e62dfc 100644
--- a/spec/services/projects/download_service_spec.rb
+++ b/spec/services/projects/download_service_spec.rb
@@ -37,7 +37,6 @@ describe Projects::DownloadService do
it { expect(@link_to_file).to have_key('url') }
it { expect(@link_to_file).to have_key('is_image') }
it { expect(@link_to_file['is_image']).to be true }
- it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
it { expect(@link_to_file['url']).to match('rails_sample.jpg') }
it { expect(@link_to_file['alt']).to eq('rails_sample') }
end
@@ -52,7 +51,6 @@ describe Projects::DownloadService do
it { expect(@link_to_file).to have_key('url') }
it { expect(@link_to_file).to have_key('is_image') }
it { expect(@link_to_file['is_image']).to be false }
- it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
it { expect(@link_to_file['url']).to match('doc_sample.txt') }
it { expect(@link_to_file['alt']).to eq('doc_sample.txt') }
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 18ab333c1d1..1feba6ce048 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -25,13 +25,6 @@ describe Projects::ForkService do
end
end
- context 'fork project failure' do
- it "fails due to transaction failure" do
- @to_project = fork_project(@from_project, @to_user, false)
- expect(@to_project.import_failed?)
- end
- end
-
context 'project already exists' do
it "should fail due to validation, not transaction failure" do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@@ -43,14 +36,10 @@ describe Projects::ForkService do
end
context 'GitLab CI is enabled' do
- it "calls fork registrator for CI" do
- create(:ci_project, gl_project: @from_project)
- @from_project.build_missing_services
- @from_project.gitlab_ci_service.update_attributes(active: true)
-
- expect_any_instance_of(Ci::CreateProjectService).to receive(:execute)
-
- fork_project(@from_project, @to_user)
+ it "fork and enable CI for fork" do
+ @from_project.enable_ci
+ @to_project = fork_project(@from_project, @to_user)
+ expect(@to_project.builds_enabled?).to be_truthy
end
end
end
@@ -70,7 +59,7 @@ describe Projects::ForkService do
context 'fork project for group' do
it 'group owner successfully forks project into the group' do
- to_project = fork_project(@project, @group_owner, true, @opts)
+ to_project = fork_project(@project, @group_owner, @opts)
expect(to_project.owner).to eq(@group)
expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name)
@@ -82,7 +71,7 @@ describe Projects::ForkService do
context 'fork project for group when user not owner' do
it 'group developer should fail to fork project into the group' do
- to_project = fork_project(@project, @developer, true, @opts)
+ to_project = fork_project(@project, @developer, @opts)
expect(to_project.errors[:namespace]).to eq(['is not valid'])
end
end
@@ -91,7 +80,7 @@ describe Projects::ForkService do
it 'should fail due to validation, not transaction failure' do
existing_project = create(:project, name: @project.name,
namespace: @group)
- to_project = fork_project(@project, @group_owner, true, @opts)
+ to_project = fork_project(@project, @group_owner, @opts)
expect(existing_project.persisted?).to be_truthy
expect(to_project.errors[:name]).to eq(['has already been taken'])
expect(to_project.errors[:path]).to eq(['has already been taken'])
@@ -99,8 +88,8 @@ describe Projects::ForkService do
end
end
- def fork_project(from_project, user, fork_success = true, params = {})
- allow(RepositoryForkWorker).to receive(:perform_async).and_return(fork_success)
+ def fork_project(from_project, user, params = {})
+ allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
Projects::ForkService.new(from_project, user, params).execute
end
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index bb7da33b12e..47755bfc990 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -7,6 +7,8 @@ describe Projects::TransferService do
context 'namespace -> namespace' do
before do
+ allow_any_instance_of(Gitlab::UploadsTransfer).
+ to receive(:move_project).and_return(true)
group.add_owner(user)
@result = transfer_project(project, user, group)
end
diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb
index fa4ff6b01ad..1b1a80d1fe7 100644
--- a/spec/services/projects/upload_service_spec.rb
+++ b/spec/services/projects/upload_service_spec.rb
@@ -18,7 +18,6 @@ describe Projects::UploadService do
it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file).to have_value('banana_sample') }
it { expect(@link_to_file[:is_image]).to equal(true) }
- it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") }
it { expect(@link_to_file[:url]).to match('banana_sample.gif') }
end
@@ -34,7 +33,6 @@ describe Projects::UploadService do
it { expect(@link_to_file).to have_value('dk') }
it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file[:is_image]).to equal(true) }
- it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") }
it { expect(@link_to_file[:url]).to match('dk.png') }
end
@@ -49,7 +47,6 @@ describe Projects::UploadService do
it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file).to have_value('rails_sample') }
it { expect(@link_to_file[:is_image]).to equal(true) }
- it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") }
it { expect(@link_to_file[:url]).to match('rails_sample.jpg') }
end
@@ -64,7 +61,6 @@ describe Projects::UploadService do
it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file).to have_value('doc_sample.txt') }
it { expect(@link_to_file[:is_image]).to equal(false) }
- it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") }
it { expect(@link_to_file[:url]).to match('doc_sample.txt') }
end
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index 48c49e2f717..a31fc1e4b07 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -13,8 +13,8 @@ describe SystemHooksService do
it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :email, :user_id) }
it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
- it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
- it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
+ it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
+ it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
it { expect(event_data(key, :create)).to include(:username, :key, :id) }
it { expect(event_data(key, :destroy)).to include(:username, :key, :id) }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 2658576640c..a45130bd473 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -242,6 +242,18 @@ describe SystemNoteService do
end
end
+ describe '.change_branch_presence' do
+ subject { described_class.change_branch_presence(noteable, project, author, :source, 'feature', :delete) }
+
+ it_behaves_like 'a system note'
+
+ context 'when source branch deleted' do
+ it 'sets the note text' do
+ expect(subject.note).to eq "Deleted source branch `feature`"
+ end
+ end
+ end
+
describe '.cross_reference' do
subject { described_class.cross_reference(noteable, mentioner, author) }
diff --git a/spec/services/update_release_service_spec.rb b/spec/services/update_release_service_spec.rb
new file mode 100644
index 00000000000..93368c45b88
--- /dev/null
+++ b/spec/services/update_release_service_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe UpdateReleaseService do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:tag_name) { project.repository.tag_names.first }
+ let(:description) { 'Awesome release!' }
+ let(:new_description) { 'The best release!' }
+ let(:service) { UpdateReleaseService.new(project, user) }
+
+ context 'with an existing release' do
+ let(:create_service) { CreateReleaseService.new(project, user) }
+
+ before do
+ create_service.execute(tag_name, description)
+ end
+
+ it 'successfully updates an existing release' do
+ result = service.execute(tag_name, new_description)
+ expect(result[:status]).to eq(:success)
+ expect(project.releases.find_by(tag: tag_name).description).to eq(new_description)
+ end
+ end
+
+ it 'raises an error if the tag does not exist' do
+ result = service.execute("foobar", description)
+ expect(result[:status]).to eq(:error)
+ end
+
+ it 'raises an error if the release does not exist' do
+ result = service.execute(tag_name, description)
+ expect(result[:status]).to eq(:error)
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index dfe855926c6..0225a0ee53f 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -14,6 +14,7 @@ require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'shoulda/matchers'
require 'sidekiq/testing/inline'
+require 'benchmark/ips'
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
@@ -30,9 +31,10 @@ RSpec.configure do |config|
config.include StubConfiguration
config.include RelativeUrl, type: feature
config.include TestEnv
+ config.include ActiveJob::TestHelper
config.include StubGitlabCalls
config.include StubGitlabData
-
+ config.include BenchmarkMatchers, benchmark: true
config.infer_spec_type_from_file_location!
config.raise_errors_for_deprecations!
@@ -42,4 +44,8 @@ RSpec.configure do |config|
end
end
+FactoryGirl::SyntaxRunner.class_eval do
+ include RSpec::Mocks::ExampleMethods
+end
+
ActiveRecord::Migration.maintain_test_schema!
diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb
index 203117aee70..91e3bee13c1 100644
--- a/spec/support/filter_spec_helper.rb
+++ b/spec/support/filter_spec_helper.rb
@@ -29,12 +29,32 @@ module FilterSpecHelper
#
# Returns the Hash
def pipeline_result(body, contexts = {})
- contexts.reverse_merge!(project: project)
+ contexts.reverse_merge!(project: project) if defined?(project)
pipeline = HTML::Pipeline.new([described_class], contexts)
pipeline.call(body)
end
+ def reference_pipeline(contexts = {})
+ contexts.reverse_merge!(project: project) if defined?(project)
+
+ filters = [
+ Gitlab::Markdown::AutolinkFilter,
+ described_class,
+ Gitlab::Markdown::ReferenceGathererFilter
+ ]
+
+ HTML::Pipeline.new(filters, contexts)
+ end
+
+ def reference_pipeline_result(body, contexts = {})
+ reference_pipeline(contexts).call(body)
+ end
+
+ def reference_filter(html, contexts = {})
+ reference_pipeline(contexts).to_document(html)
+ end
+
# Modify a String reference to make it invalid
#
# Commit SHAs get reversed, IDs get incremented by 1, all other Strings get
@@ -55,20 +75,6 @@ module FilterSpecHelper
end
end
- # Stub CrossProjectReference#user_can_reference_project? to return true for
- # the current test
- def allow_cross_reference!
- allow_any_instance_of(described_class).
- to receive(:user_can_reference_project?).and_return(true)
- end
-
- # Stub CrossProjectReference#user_can_reference_project? to return false for
- # the current test
- def disallow_cross_reference!
- allow_any_instance_of(described_class).
- to receive(:user_can_reference_project?).and_return(false)
- end
-
# Shortcut to Rails' auto-generated routes helpers, to avoid including the
# module
def urls
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index 39a64391460..d6d3062a197 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -15,18 +15,17 @@ class MarkdownFeature
end
def group
- unless @group
- @group = create(:group)
- @group.add_developer(user)
+ @group ||= create(:group).tap do |group|
+ group.add_developer(user)
end
-
- @group
end
# Direct references ----------------------------------------------------------
def project
- @project ||= create(:project)
+ @project ||= create(:project).tap do |project|
+ project.team << [user, :master]
+ end
end
def issue
@@ -46,12 +45,10 @@ class MarkdownFeature
end
def commit_range
- unless @commit_range
+ @commit_range ||= begin
commit2 = project.commit('HEAD~3')
- @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project)
+ CommitRange.new("#{commit.id}...#{commit2.id}", project)
end
-
- @commit_range
end
def simple_label
@@ -65,13 +62,12 @@ class MarkdownFeature
# Cross-references -----------------------------------------------------------
def xproject
- unless @xproject
+ @xproject ||= begin
namespace = create(:namespace, name: 'cross-reference')
- @xproject = create(:project, namespace: namespace)
- @xproject.team << [user, :developer]
+ create(:project, namespace: namespace) do |project|
+ project.team << [user, :developer]
+ end
end
-
- @xproject
end
def xissue
@@ -91,12 +87,14 @@ class MarkdownFeature
end
def xcommit_range
- unless @xcommit_range
+ @xcommit_range ||= begin
xcommit2 = xproject.commit('HEAD~2')
- @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject)
+ CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject)
end
+ end
- @xcommit_range
+ def urls
+ Gitlab::Application.routes.url_helpers
end
def raw_markdown
diff --git a/spec/support/matchers/benchmark_matchers.rb b/spec/support/matchers/benchmark_matchers.rb
new file mode 100644
index 00000000000..84f655c2119
--- /dev/null
+++ b/spec/support/matchers/benchmark_matchers.rb
@@ -0,0 +1,61 @@
+module BenchmarkMatchers
+ extend RSpec::Matchers::DSL
+
+ def self.included(into)
+ into.extend(ClassMethods)
+ end
+
+ matcher :iterate_per_second do |min_iterations|
+ supports_block_expectations
+
+ match do |block|
+ @max_stddev ||= 30
+
+ @entry = benchmark(&block)
+
+ expect(@entry.ips).to be >= min_iterations
+ expect(@entry.stddev_percentage).to be <= @max_stddev
+ end
+
+ chain :with_maximum_stddev do |value|
+ @max_stddev = value
+ end
+
+ description do
+ "run at least #{min_iterations} iterations per second"
+ end
+
+ failure_message do
+ ips = @entry.ips.round(2)
+ stddev = @entry.stddev_percentage.round(2)
+
+ "expected at least #{min_iterations} iterations per second " \
+ "with a maximum stddev of #{@max_stddev}%, instead of " \
+ "#{ips} iterations per second with a stddev of #{stddev}%"
+ end
+ end
+
+ # Benchmarks the given block and returns a Benchmark::IPS::Report::Entry.
+ def benchmark(&block)
+ report = Benchmark.ips(quiet: true) do |bench|
+ bench.report do
+ instance_eval(&block)
+ end
+ end
+
+ report.entries[0]
+ end
+
+ module ClassMethods
+ # Wraps around rspec's subject method so you can write:
+ #
+ # benchmark_subject { SomeClass.some_method }
+ #
+ # instead of:
+ #
+ # subject { -> { SomeClass.some_method } }
+ def benchmark_subject(&block)
+ subject { block }
+ end
+ end
+end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 7500d0fdf80..7eadcd58c1f 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -71,7 +71,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-project_member', count: 3)
+ expect(actual).to have_selector('a.gfm.gfm-project_member', count: 4)
end
end
@@ -80,7 +80,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-issue', count: 3)
+ expect(actual).to have_selector('a.gfm.gfm-issue', count: 6)
end
end
@@ -89,7 +89,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 3)
+ expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 6)
expect(actual).to have_selector('em a.gfm-merge_request')
end
end
@@ -99,7 +99,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-snippet', count: 2)
+ expect(actual).to have_selector('a.gfm.gfm-snippet', count: 5)
end
end
@@ -108,7 +108,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 2)
+ expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 5)
end
end
@@ -117,7 +117,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-commit', count: 2)
+ expect(actual).to have_selector('a.gfm.gfm-commit', count: 5)
end
end
@@ -126,7 +126,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-label', count: 3)
+ expect(actual).to have_selector('a.gfm.gfm-label', count: 4)
end
end
diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb
index e3de0afb448..33d2b14583c 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/mentionable_shared_examples.rb
@@ -5,17 +5,17 @@
# - let(:set_mentionable_text) { lambda { |txt| "block that assigns txt to the subject's mentionable_text" } }
def common_mentionable_setup
- let(:project) { create :project }
+ let(:project) { subject.project }
let(:author) { subject.author }
let(:mentioned_issue) { create(:issue, project: project) }
let!(:mentioned_mr) { create(:merge_request, :simple, source_project: project) }
- let(:mentioned_commit) { project.commit }
+ let(:mentioned_commit) { project.commit("HEAD~1") }
let(:ext_proj) { create(:project, :public) }
let(:ext_issue) { create(:issue, project: ext_proj) }
let(:ext_mr) { create(:merge_request, :simple, source_project: ext_proj) }
- let(:ext_commit) { ext_proj.commit }
+ let(:ext_commit) { ext_proj.commit("HEAD~2") }
# Override to add known commits to the repository stub.
let(:extra_commits) { [] }
@@ -45,12 +45,11 @@ def common_mentionable_setup
before do
# Wire the project's repository to return the mentioned commit, and +nil+
# for any unrecognized commits.
- commitmap = {
- mentioned_commit.id => mentioned_commit
- }
- extra_commits.each { |c| commitmap[c.short_id] = c }
-
- allow(project.repository).to receive(:commit) { |sha| commitmap[sha] }
+ allow_any_instance_of(::Repository).to receive(:commit).and_call_original
+ allow_any_instance_of(::Repository).to receive(:commit).with(mentioned_commit.short_id).and_return(mentioned_commit)
+ extra_commits.each do |commit|
+ allow_any_instance_of(::Repository).to receive(:commit).with(commit.short_id).and_return(commit)
+ end
set_mentionable_text.call(ref_string)
end
@@ -65,7 +64,7 @@ shared_examples 'a mentionable' do
it "extracts references from its reference property" do
# De-duplicate and omit itself
- refs = subject.references(project)
+ refs = subject.referenced_mentionables
expect(refs.size).to eq(6)
expect(refs).to include(mentioned_issue)
expect(refs).to include(mentioned_mr)
@@ -84,14 +83,7 @@ shared_examples 'a mentionable' do
with(referenced, subject.local_reference, author)
end
- subject.create_cross_references!(project, author)
- end
-
- it 'detects existing cross-references' do
- SystemNoteService.cross_reference(mentioned_issue, subject.local_reference, author)
-
- expect(subject).to have_mentioned(mentioned_issue)
- expect(subject).not_to have_mentioned(mentioned_mr)
+ subject.create_cross_references!
end
end
@@ -143,6 +135,6 @@ shared_examples 'an editable mentionable' do
end
set_mentionable_text.call(new_text)
- subject.create_new_cross_references!(project, author)
+ subject.create_new_cross_references!(author)
end
end
diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb
index 4d007ae55ee..d1c999cad4d 100644
--- a/spec/support/services_shared_context.rb
+++ b/spec/support/services_shared_context.rb
@@ -3,7 +3,13 @@ Service.available_services_names.each do |service|
let(:dashed_service) { service.dasherize }
let(:service_method) { "#{service}_service".to_sym }
let(:service_klass) { "#{service}_service".classify.constantize }
- let(:service_attrs_list) { service_klass.new.fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } }
+ let(:service_fields) { service_klass.new.fields }
+ let(:service_attrs_list) { service_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } }
+ let(:service_attrs_list_without_passwords) do
+ service_fields.
+ select { |field| field[:type] != 'password' }.
+ map { |field| field[:name].to_sym}
+ end
let(:service_attrs) do
service_attrs_list.inject({}) do |hash, k|
if k =~ /^(token*|.*_token|.*_key)/
diff --git a/spec/support/setup_builds_storage.rb b/spec/support/setup_builds_storage.rb
index a3e59646187..a4f21e95338 100644
--- a/spec/support/setup_builds_storage.rb
+++ b/spec/support/setup_builds_storage.rb
@@ -10,8 +10,10 @@ RSpec.configure do |config|
end
config.after(:suite) do
- Dir.chdir(builds_path) do
- `ls | grep -v .gitkeep | xargs rm -r`
+ Dir[File.join(builds_path, '*')].each do |path|
+ next if File.basename(path) == '.gitkeep'
+
+ FileUtils.rm_rf(path)
end
end
end
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index 5e6744afda1..5b3eb1bfc5f 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -13,6 +13,14 @@ module StubGitlabCalls
allow_any_instance_of(Network).to receive(:projects) { project_hash_array }
end
+ def stub_ci_commit_to_return_yaml_file
+ stub_ci_commit_yaml_file(gitlab_ci_yaml)
+ end
+
+ def stub_ci_commit_yaml_file(ci_yaml)
+ allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { ci_yaml }
+ end
+
private
def gitlab_url
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 3eab74ba986..78b9a0f42fa 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -9,7 +9,7 @@ module TestEnv
'flatten-dir' => 'e56497b',
'feature' => '0b4bc9a',
'feature_conflict' => 'bb5206f',
- 'fix' => '12d65c8',
+ 'fix' => '48f0be4',
'improve/awesome' => '5937ac0',
'markdown' => '0ed8c6c',
'master' => '5937ac0',
@@ -21,7 +21,8 @@ module TestEnv
# We currently only need a subset of the branches
FORKED_BRANCH_SHA = {
'add-submodule-version-bump' => '3f547c08',
- 'master' => '5937ac0'
+ 'master' => '5937ac0',
+ 'remove-submodule' => '2a33e0c0'
}
# Test environment
@@ -96,15 +97,15 @@ module TestEnv
clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git"
unless File.directory?(repo_path)
- system(*%W(git clone -q #{clone_url} #{repo_path}))
+ system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
end
Dir.chdir(repo_path) do
branch_sha.each do |branch, sha|
# Try to reset without fetching to avoid using the network.
- reset = %W(git update-ref refs/heads/#{branch} #{sha})
+ reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha})
unless system(*reset)
- if system(*%w(git fetch origin))
+ if system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
unless system(*reset)
raise 'The fetched test seed '\
'does not contain the required revision.'
@@ -117,7 +118,7 @@ module TestEnv
end
# We must copy bare repositories because we will push to them.
- system(git_env, *%W(git clone -q --bare #{repo_path} #{repo_path_bare}))
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
end
def copy_repo(project)
diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb
new file mode 100644
index 00000000000..692d219e9f1
--- /dev/null
+++ b/spec/support/wait_for_ajax.rb
@@ -0,0 +1,11 @@
+module WaitForAjax
+ def wait_for_ajax
+ Timeout.timeout(Capybara.default_wait_time) do
+ loop until finished_all_ajax_requests?
+ end
+ end
+
+ def finished_all_ajax_requests?
+ page.evaluate_script('jQuery.active').zero?
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 2e63e5f36af..63bed2414df 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -16,7 +16,7 @@ describe 'gitlab:app namespace rake task' do
end
def reenable_backup_sub_tasks
- %w{db repo uploads builds}.each do |subtask|
+ %w{db repo uploads builds artifacts lfs}.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@@ -49,12 +49,15 @@ describe 'gitlab:app namespace rake task' do
to raise_error(SystemExit)
end
- it 'should invoke restoration on mach' do
+ it 'should invoke restoration on match' do
allow(YAML).to receive(:load_file).
and_return({ gitlab_version: gitlab_version })
expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:backup:repo:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke)
+ expect(Rake::Task["gitlab:backup:uploads:restore"]).to receive(:invoke)
+ expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive(:invoke)
+ expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke)
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
@@ -112,19 +115,21 @@ describe 'gitlab:app namespace rake task' do
it 'should set correct permissions on the tar contents' do
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads repositories builds}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz}
)
expect(exit_status).to eq(0)
expect(tar_contents).to match('db/')
- expect(tar_contents).to match('uploads/')
+ expect(tar_contents).to match('uploads.tar.gz')
expect(tar_contents).to match('repositories/')
- expect(tar_contents).to match('builds/')
- expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories|builds)\/$/)
+ expect(tar_contents).to match('builds.tar.gz')
+ expect(tar_contents).to match('artifacts.tar.gz')
+ expect(tar_contents).to match('lfs.tar.gz')
+ expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz)\/$/)
end
it 'should delete temp directories' do
temp_dirs = Dir.glob(
- File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds}')
+ File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs}')
)
expect(temp_dirs).to be_empty
@@ -146,7 +151,7 @@ describe 'gitlab:app namespace rake task' do
# Redirect STDOUT and run the rake task
orig_stdout = $stdout
$stdout = StringIO.new
- ENV["SKIP"] = "repositories"
+ ENV["SKIP"] = "repositories,uploads"
run_rake_task('gitlab:backup:create')
$stdout = orig_stdout
@@ -159,13 +164,15 @@ describe 'gitlab:app namespace rake task' do
end
it "does not contain skipped item" do
- tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads repositories builds}
+ tar_contents, _exit_status = Gitlab::Popen.popen(
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz}
)
expect(tar_contents).to match('db/')
- expect(tar_contents).to match('uploads/')
- expect(tar_contents).to match('builds/')
+ expect(tar_contents).to match('uploads.tar.gz')
+ expect(tar_contents).to match('builds.tar.gz')
+ expect(tar_contents).to match('artifacts.tar.gz')
+ expect(tar_contents).to match('lfs.tar.gz')
expect(tar_contents).not_to match('repositories/')
end
@@ -176,7 +183,10 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke
expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke
+ expect(Rake::Task["gitlab:backup:uploads:restore"]).not_to receive :invoke
expect(Rake::Task["gitlab:backup:builds:restore"]).to receive :invoke
+ expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive :invoke
+ expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive :invoke
expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
index 65a8d7d9197..de40a6f78af 100644
--- a/spec/workers/email_receiver_worker_spec.rb
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -21,12 +21,14 @@ describe EmailReceiverWorker do
end
it "sends out a rejection email" do
- described_class.new.perform(raw_message)
-
- email = ActionMailer::Base.deliveries.last
- expect(email).not_to be_nil
- expect(email.to).to eq(["jake@adventuretime.ooo"])
- expect(email.subject).to include("Rejected")
+ perform_enqueued_jobs do
+ described_class.new.perform(raw_message)
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email).not_to be_nil
+ expect(email.to).to eq(["jake@adventuretime.ooo"])
+ expect(email.subject).to include("Rejected")
+ end
end
end
end
diff --git a/spec/workers/repository_archive_worker_spec.rb b/spec/workers/repository_archive_worker_spec.rb
deleted file mode 100644
index a914d0ac8dc..00000000000
--- a/spec/workers/repository_archive_worker_spec.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-require 'spec_helper'
-
-describe RepositoryArchiveWorker do
- let(:project) { create(:project) }
- subject { RepositoryArchiveWorker.new }
-
- before do
- allow(Project).to receive(:find).and_return(project)
- end
-
- describe "#perform" do
- it "cleans old archives" do
- expect(project.repository).to receive(:clean_old_archives)
-
- subject.perform(project.id, "master", "zip")
- end
-
- context "when the repository doesn't have an archive file path" do
- before do
- allow(project.repository).to receive(:archive_file_path).and_return(nil)
- end
-
- it "doesn't archive the repo" do
- expect(project.repository).not_to receive(:archive_repo)
-
- subject.perform(project.id, "master", "zip")
- end
- end
-
- context "when the repository has an archive file path" do
- let(:file_path) { "/archive.zip" }
- let(:pid_file_path) { "/archive.zip.pid" }
-
- before do
- allow(project.repository).to receive(:archive_file_path).and_return(file_path)
- allow(project.repository).to receive(:archive_pid_file_path).and_return(pid_file_path)
- end
-
- context "when the archive file already exists" do
- before do
- allow(File).to receive(:exist?).with(file_path).and_return(true)
- end
-
- it "doesn't archive the repo" do
- expect(project.repository).not_to receive(:archive_repo)
-
- subject.perform(project.id, "master", "zip")
- end
- end
-
- context "when the archive file doesn't exist yet" do
- before do
- allow(File).to receive(:exist?).with(file_path).and_return(false)
- allow(File).to receive(:exist?).with(pid_file_path).and_return(true)
- end
-
- context "when the archive pid file doesn't exist yet" do
- before do
- allow(File).to receive(:exist?).with(pid_file_path).and_return(false)
- end
-
- it "archives the repo" do
- expect(project.repository).to receive(:archive_repo)
-
- subject.perform(project.id, "master", "zip")
- end
- end
-
- context "when the archive pid file already exists" do
- it "doesn't archive the repo" do
- expect(project.repository).not_to receive(:archive_repo)
-
- subject.perform(project.id, "master", "zip")
- end
- end
- end
- end
- end
-end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index aa031106968..245f066df1f 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -12,7 +12,6 @@ describe RepositoryForkWorker do
project.path_with_namespace,
fork_project.namespace.path).
and_return(true)
- expect(ProjectCacheWorker).to receive(:perform_async)
subject.perform(project.id,
project.path_with_namespace,
diff --git a/spec/workers/stuck_ci_builds_worker_spec.rb b/spec/workers/stuck_ci_builds_worker_spec.rb
new file mode 100644
index 00000000000..f9d87d97014
--- /dev/null
+++ b/spec/workers/stuck_ci_builds_worker_spec.rb
@@ -0,0 +1,44 @@
+require "spec_helper"
+
+describe StuckCiBuildsWorker do
+ let!(:build) { create :ci_build }
+
+ subject do
+ build.reload
+ build.status
+ end
+
+ %w(pending running).each do |status|
+ context "#{status} build" do
+ before do
+ build.update!(status: status)
+ end
+
+ it 'gets dropped if it was updated over 2 days ago' do
+ build.update!(updated_at: 2.day.ago)
+ StuckCiBuildsWorker.new.perform
+ is_expected.to eq('failed')
+ end
+
+ it "is still #{status}" do
+ build.update!(updated_at: 1.minute.ago)
+ StuckCiBuildsWorker.new.perform
+ is_expected.to eq(status)
+ end
+ end
+ end
+
+ %w(success failed canceled).each do |status|
+ context "#{status} build" do
+ before do
+ build.update!(status: status)
+ end
+
+ it "is still #{status}" do
+ build.update!(updated_at: 2.day.ago)
+ StuckCiBuildsWorker.new.perform
+ is_expected.to eq(status)
+ end
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/clipboard.js b/vendor/assets/javascripts/clipboard.js
new file mode 100644
index 00000000000..1b1f4f0bd63
--- /dev/null
+++ b/vendor/assets/javascripts/clipboard.js
@@ -0,0 +1,621 @@
+/*!
+ * clipboard.js v1.4.2
+ * https://zenorocha.github.io/clipboard.js
+ *
+ * Licensed MIT © Zeno Rocha
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/**
+ * Module dependencies.
+ */
+
+var closest = require('closest')
+ , event = require('component-event');
+
+/**
+ * Delegate event `type` to `selector`
+ * and invoke `fn(e)`. A callback function
+ * is returned which may be passed to `.unbind()`.
+ *
+ * @param {Element} el
+ * @param {String} selector
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @return {Function}
+ * @api public
+ */
+
+// Some events don't bubble, so we want to bind to the capture phase instead
+// when delegating.
+var forceCaptureEvents = ['focus', 'blur'];
+
+exports.bind = function(el, selector, type, fn, capture){
+ if (forceCaptureEvents.indexOf(type) !== -1) capture = true;
+
+ return event.bind(el, type, function(e){
+ var target = e.target || e.srcElement;
+ e.delegateTarget = closest(target, selector, true, el);
+ if (e.delegateTarget) fn.call(el, e);
+ }, capture);
+};
+
+/**
+ * Unbind event `type`'s callback `fn`.
+ *
+ * @param {Element} el
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @api public
+ */
+
+exports.unbind = function(el, type, fn, capture){
+ if (forceCaptureEvents.indexOf(type) !== -1) capture = true;
+
+ event.unbind(el, type, fn, capture);
+};
+
+},{"closest":2,"component-event":4}],2:[function(require,module,exports){
+var matches = require('matches-selector')
+
+module.exports = function (element, selector, checkYoSelf) {
+ var parent = checkYoSelf ? element : element.parentNode
+
+ while (parent && parent !== document) {
+ if (matches(parent, selector)) return parent;
+ parent = parent.parentNode
+ }
+}
+
+},{"matches-selector":3}],3:[function(require,module,exports){
+
+/**
+ * Element prototype.
+ */
+
+var proto = Element.prototype;
+
+/**
+ * Vendor function.
+ */
+
+var vendor = proto.matchesSelector
+ || proto.webkitMatchesSelector
+ || proto.mozMatchesSelector
+ || proto.msMatchesSelector
+ || proto.oMatchesSelector;
+
+/**
+ * Expose `match()`.
+ */
+
+module.exports = match;
+
+/**
+ * Match `el` to `selector`.
+ *
+ * @param {Element} el
+ * @param {String} selector
+ * @return {Boolean}
+ * @api public
+ */
+
+function match(el, selector) {
+ if (vendor) return vendor.call(el, selector);
+ var nodes = el.parentNode.querySelectorAll(selector);
+ for (var i = 0; i < nodes.length; ++i) {
+ if (nodes[i] == el) return true;
+ }
+ return false;
+}
+},{}],4:[function(require,module,exports){
+var bind = window.addEventListener ? 'addEventListener' : 'attachEvent',
+ unbind = window.removeEventListener ? 'removeEventListener' : 'detachEvent',
+ prefix = bind !== 'addEventListener' ? 'on' : '';
+
+/**
+ * Bind `el` event `type` to `fn`.
+ *
+ * @param {Element} el
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @return {Function}
+ * @api public
+ */
+
+exports.bind = function(el, type, fn, capture){
+ el[bind](prefix + type, fn, capture || false);
+ return fn;
+};
+
+/**
+ * Unbind `el` event `type`'s callback `fn`.
+ *
+ * @param {Element} el
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @return {Function}
+ * @api public
+ */
+
+exports.unbind = function(el, type, fn, capture){
+ el[unbind](prefix + type, fn, capture || false);
+ return fn;
+};
+},{}],5:[function(require,module,exports){
+function E () {
+ // Keep this empty so it's easier to inherit from
+ // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
+}
+
+E.prototype = {
+ on: function (name, callback, ctx) {
+ var e = this.e || (this.e = {});
+
+ (e[name] || (e[name] = [])).push({
+ fn: callback,
+ ctx: ctx
+ });
+
+ return this;
+ },
+
+ once: function (name, callback, ctx) {
+ var self = this;
+ var fn = function () {
+ self.off(name, fn);
+ callback.apply(ctx, arguments);
+ };
+
+ return this.on(name, fn, ctx);
+ },
+
+ emit: function (name) {
+ var data = [].slice.call(arguments, 1);
+ var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
+ var i = 0;
+ var len = evtArr.length;
+
+ for (i; i < len; i++) {
+ evtArr[i].fn.apply(evtArr[i].ctx, data);
+ }
+
+ return this;
+ },
+
+ off: function (name, callback) {
+ var e = this.e || (this.e = {});
+ var evts = e[name];
+ var liveEvents = [];
+
+ if (evts && callback) {
+ for (var i = 0, len = evts.length; i < len; i++) {
+ if (evts[i].fn !== callback) liveEvents.push(evts[i]);
+ }
+ }
+
+ // Remove event from queue to prevent memory leak
+ // Suggested by https://github.com/lazd
+ // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
+
+ (liveEvents.length)
+ ? e[name] = liveEvents
+ : delete e[name];
+
+ return this;
+ }
+};
+
+module.exports = E;
+
+},{}],6:[function(require,module,exports){
+/**
+ * Inner class which performs selection from either `text` or `target`
+ * properties and then executes copy or cut operations.
+ */
+'use strict';
+
+exports.__esModule = true;
+
+var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+var ClipboardAction = (function () {
+ /**
+ * @param {Object} options
+ */
+
+ function ClipboardAction(options) {
+ _classCallCheck(this, ClipboardAction);
+
+ this.resolveOptions(options);
+ this.initSelection();
+ }
+
+ /**
+ * Defines base properties passed from constructor.
+ * @param {Object} options
+ */
+
+ ClipboardAction.prototype.resolveOptions = function resolveOptions() {
+ var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+
+ this.action = options.action;
+ this.emitter = options.emitter;
+ this.target = options.target;
+ this.text = options.text;
+ this.trigger = options.trigger;
+
+ this.selectedText = '';
+ };
+
+ /**
+ * Decides which selection strategy is going to be applied based
+ * on the existence of `text` and `target` properties.
+ */
+
+ ClipboardAction.prototype.initSelection = function initSelection() {
+ if (this.text && this.target) {
+ throw new Error('Multiple attributes declared, use either "target" or "text"');
+ } else if (this.text) {
+ this.selectFake();
+ } else if (this.target) {
+ this.selectTarget();
+ } else {
+ throw new Error('Missing required attributes, use either "target" or "text"');
+ }
+ };
+
+ /**
+ * Creates a fake textarea element, sets its value from `text` property,
+ * and makes a selection on it.
+ */
+
+ ClipboardAction.prototype.selectFake = function selectFake() {
+ var _this = this;
+
+ this.removeFake();
+
+ this.fakeHandler = document.body.addEventListener('click', function () {
+ return _this.removeFake();
+ });
+
+ this.fakeElem = document.createElement('textarea');
+ this.fakeElem.style.position = 'absolute';
+ this.fakeElem.style.left = '-9999px';
+ this.fakeElem.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
+ this.fakeElem.setAttribute('readonly', '');
+ this.fakeElem.value = this.text;
+ this.selectedText = this.text;
+
+ document.body.appendChild(this.fakeElem);
+
+ this.fakeElem.select();
+ this.copyText();
+ };
+
+ /**
+ * Only removes the fake element after another click event, that way
+ * a user can hit `Ctrl+C` to copy because selection still exists.
+ */
+
+ ClipboardAction.prototype.removeFake = function removeFake() {
+ if (this.fakeHandler) {
+ document.body.removeEventListener('click');
+ this.fakeHandler = null;
+ }
+
+ if (this.fakeElem) {
+ document.body.removeChild(this.fakeElem);
+ this.fakeElem = null;
+ }
+ };
+
+ /**
+ * Selects the content from element passed on `target` property.
+ */
+
+ ClipboardAction.prototype.selectTarget = function selectTarget() {
+ if (this.target.nodeName === 'INPUT' || this.target.nodeName === 'TEXTAREA') {
+ this.target.select();
+ this.selectedText = this.target.value;
+ } else {
+ var range = document.createRange();
+ var selection = window.getSelection();
+
+ selection.removeAllRanges();
+ range.selectNodeContents(this.target);
+ selection.addRange(range);
+ this.selectedText = selection.toString();
+ }
+
+ this.copyText();
+ };
+
+ /**
+ * Executes the copy operation based on the current selection.
+ */
+
+ ClipboardAction.prototype.copyText = function copyText() {
+ var succeeded = undefined;
+
+ try {
+ succeeded = document.execCommand(this.action);
+ } catch (err) {
+ succeeded = false;
+ }
+
+ this.handleResult(succeeded);
+ };
+
+ /**
+ * Fires an event based on the copy operation result.
+ * @param {Boolean} succeeded
+ */
+
+ ClipboardAction.prototype.handleResult = function handleResult(succeeded) {
+ if (succeeded) {
+ this.emitter.emit('success', {
+ action: this.action,
+ text: this.selectedText,
+ trigger: this.trigger,
+ clearSelection: this.clearSelection.bind(this)
+ });
+ } else {
+ this.emitter.emit('error', {
+ action: this.action,
+ trigger: this.trigger,
+ clearSelection: this.clearSelection.bind(this)
+ });
+ }
+ };
+
+ /**
+ * Removes current selection and focus from `target` element.
+ */
+
+ ClipboardAction.prototype.clearSelection = function clearSelection() {
+ if (this.target) {
+ this.target.blur();
+ }
+
+ window.getSelection().removeAllRanges();
+ };
+
+ /**
+ * Sets the `action` to be performed which can be either 'copy' or 'cut'.
+ * @param {String} action
+ */
+
+ /**
+ * Destroy lifecycle.
+ */
+
+ ClipboardAction.prototype.destroy = function destroy() {
+ this.removeFake();
+ };
+
+ _createClass(ClipboardAction, [{
+ key: 'action',
+ set: function set() {
+ var action = arguments.length <= 0 || arguments[0] === undefined ? 'copy' : arguments[0];
+
+ this._action = action;
+
+ if (this._action !== 'copy' && this._action !== 'cut') {
+ throw new Error('Invalid "action" value, use either "copy" or "cut"');
+ }
+ },
+
+ /**
+ * Gets the `action` property.
+ * @return {String}
+ */
+ get: function get() {
+ return this._action;
+ }
+
+ /**
+ * Sets the `target` property using an element
+ * that will be have its content copied.
+ * @param {Element} target
+ */
+ }, {
+ key: 'target',
+ set: function set(target) {
+ if (target !== undefined) {
+ if (target && typeof target === 'object' && target.nodeType === 1) {
+ this._target = target;
+ } else {
+ throw new Error('Invalid "target" value, use a valid Element');
+ }
+ }
+ },
+
+ /**
+ * Gets the `target` property.
+ * @return {String|HTMLElement}
+ */
+ get: function get() {
+ return this._target;
+ }
+ }]);
+
+ return ClipboardAction;
+})();
+
+exports['default'] = ClipboardAction;
+module.exports = exports['default'];
+
+},{}],7:[function(require,module,exports){
+'use strict';
+
+exports.__esModule = true;
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+var _clipboardAction = require('./clipboard-action');
+
+var _clipboardAction2 = _interopRequireDefault(_clipboardAction);
+
+var _delegateEvents = require('delegate-events');
+
+var _delegateEvents2 = _interopRequireDefault(_delegateEvents);
+
+var _tinyEmitter = require('tiny-emitter');
+
+var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter);
+
+/**
+ * Base class which takes a selector, delegates a click event to it,
+ * and instantiates a new `ClipboardAction` on each click.
+ */
+
+var Clipboard = (function (_Emitter) {
+ _inherits(Clipboard, _Emitter);
+
+ /**
+ * @param {String} selector
+ * @param {Object} options
+ */
+
+ function Clipboard(selector, options) {
+ _classCallCheck(this, Clipboard);
+
+ _Emitter.call(this);
+
+ this.resolveOptions(options);
+ this.delegateClick(selector);
+ }
+
+ /**
+ * Helper function to retrieve attribute value.
+ * @param {String} suffix
+ * @param {Element} element
+ */
+
+ /**
+ * Defines if attributes would be resolved using internal setter functions
+ * or custom functions that were passed in the constructor.
+ * @param {Object} options
+ */
+
+ Clipboard.prototype.resolveOptions = function resolveOptions() {
+ var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+
+ this.action = typeof options.action === 'function' ? options.action : this.defaultAction;
+ this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;
+ this.text = typeof options.text === 'function' ? options.text : this.defaultText;
+ };
+
+ /**
+ * Delegates a click event on the passed selector.
+ * @param {String} selector
+ */
+
+ Clipboard.prototype.delegateClick = function delegateClick(selector) {
+ var _this = this;
+
+ this.binding = _delegateEvents2['default'].bind(document.body, selector, 'click', function (e) {
+ return _this.onClick(e);
+ });
+ };
+
+ /**
+ * Undelegates a click event on body.
+ * @param {String} selector
+ */
+
+ Clipboard.prototype.undelegateClick = function undelegateClick() {
+ _delegateEvents2['default'].unbind(document.body, 'click', this.binding);
+ };
+
+ /**
+ * Defines a new `ClipboardAction` on each click event.
+ * @param {Event} e
+ */
+
+ Clipboard.prototype.onClick = function onClick(e) {
+ if (this.clipboardAction) {
+ this.clipboardAction = null;
+ }
+
+ this.clipboardAction = new _clipboardAction2['default']({
+ action: this.action(e.delegateTarget),
+ target: this.target(e.delegateTarget),
+ text: this.text(e.delegateTarget),
+ trigger: e.delegateTarget,
+ emitter: this
+ });
+ };
+
+ /**
+ * Default `action` lookup function.
+ * @param {Element} trigger
+ */
+
+ Clipboard.prototype.defaultAction = function defaultAction(trigger) {
+ return getAttributeValue('action', trigger);
+ };
+
+ /**
+ * Default `target` lookup function.
+ * @param {Element} trigger
+ */
+
+ Clipboard.prototype.defaultTarget = function defaultTarget(trigger) {
+ var selector = getAttributeValue('target', trigger);
+
+ if (selector) {
+ return document.querySelector(selector);
+ }
+ };
+
+ /**
+ * Default `text` lookup function.
+ * @param {Element} trigger
+ */
+
+ Clipboard.prototype.defaultText = function defaultText(trigger) {
+ return getAttributeValue('text', trigger);
+ };
+
+ /**
+ * Destroy lifecycle.
+ */
+
+ Clipboard.prototype.destroy = function destroy() {
+ this.undelegateClick();
+
+ if (this.clipboardAction) {
+ this.clipboardAction.destroy();
+ this.clipboardAction = null;
+ }
+ };
+
+ return Clipboard;
+})(_tinyEmitter2['default']);
+
+function getAttributeValue(suffix, element) {
+ var attribute = 'data-clipboard-' + suffix;
+
+ if (!element.hasAttribute(attribute)) {
+ return;
+ }
+
+ return element.getAttribute(attribute);
+}
+
+exports['default'] = Clipboard;
+module.exports = exports['default'];
+
+},{"./clipboard-action":6,"delegate-events":1,"tiny-emitter":5}]},{},[7])(7)
+}); \ No newline at end of file