summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2016-10-03 09:10:20 +0100
committerPhil Hughes <me@iamphill.com>2016-10-03 09:10:20 +0100
commitcc88fa4d5be9ec0d5fb42f1bb5efa0b82f30a589 (patch)
tree1b9facaf15466aa7f5b0393e97cb1eabfdcafa7e
parent69db604e55de2bdf1a28c274be6cc9131534517d (diff)
parentf2c0f8237124d2dc539120bd77f301f216453cb7 (diff)
downloadgitlab-ce-cc88fa4d5be9ec0d5fb42f1bb5efa0b82f30a589.tar.gz
Merge branch 'master' into revert-c676283b
-rw-r--r--.flayignore2
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml53
-rw-r--r--.gitlab/issue_templates/Bug.md2
-rw-r--r--.haml-lint.yml103
-rw-r--r--.pkgr.yml12
-rw-r--r--.rubocop_todo.yml125
-rw-r--r--CHANGELOG1948
-rw-r--r--CONTRIBUTING.md20
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile35
-rw-r--r--Gemfile.lock90
-rw-r--r--README.md3
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/icon-link.pngbin729 -> 0 bytes
-rw-r--r--app/assets/images/icon_anchor.svg1
-rw-r--r--app/assets/javascripts/LabelManager.js5
-rw-r--r--app/assets/javascripts/api.js14
-rw-r--r--app/assets/javascripts/application.js67
-rw-r--r--app/assets/javascripts/autosave.js6
-rw-r--r--app/assets/javascripts/awards_handler.js17
-rw-r--r--app/assets/javascripts/behaviors/autosize.js2
-rw-r--r--app/assets/javascripts/behaviors/details_behavior.js6
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js20
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js19
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js7
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js3
-rw-r--r--app/assets/javascripts/blob/template_selector.js10
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js17
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es65
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es69
-rw-r--r--app/assets/javascripts/breakpoints.js2
-rw-r--r--app/assets/javascripts/build.js25
-rw-r--r--app/assets/javascripts/build_variables.js.es66
-rw-r--r--app/assets/javascripts/commit/image-file.js2
-rw-r--r--app/assets/javascripts/commits.js1
-rw-r--r--app/assets/javascripts/compare_autocomplete.js5
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js7
-rw-r--r--app/assets/javascripts/cycle-analytics.js.es693
-rw-r--r--app/assets/javascripts/diff.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es68
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es66
-rw-r--r--app/assets/javascripts/diff_notes/mixins/namespace.js.es69
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es626
-rw-r--r--app/assets/javascripts/dispatcher.js9
-rw-r--r--app/assets/javascripts/due_date_select.js3
-rw-r--r--app/assets/javascripts/extensions/jquery.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es624
-rw-r--r--app/assets/javascripts/gl_dropdown.js109
-rw-r--r--app/assets/javascripts/gl_form.js6
-rw-r--r--app/assets/javascripts/graphs/graphs_bundle.js8
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js1
-rw-r--r--app/assets/javascripts/groups_select.js1
-rw-r--r--app/assets/javascripts/importer_status.js17
-rw-r--r--app/assets/javascripts/issuable.js.es6 (renamed from app/assets/javascripts/issuable.js)78
-rw-r--r--app/assets/javascripts/issue.js12
-rw-r--r--app/assets/javascripts/issues-bulk-assignment.js10
-rw-r--r--app/assets/javascripts/labels.js3
-rw-r--r--app/assets/javascripts/labels_select.js29
-rw-r--r--app/assets/javascripts/layout_nav.js25
-rw-r--r--app/assets/javascripts/lib/chart.js1
-rw-r--r--app/assets/javascripts/lib/cropper.js1
-rw-r--r--app/assets/javascripts/lib/d3.js1
-rw-r--r--app/assets/javascripts/lib/raphael.js5
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js1
-rw-r--r--app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb2
-rw-r--r--app/assets/javascripts/lib/utils/emoji_aliases.js.erb6
-rw-r--r--app/assets/javascripts/lib/utils/notify.js5
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js5
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js8
-rw-r--r--app/assets/javascripts/line_highlighter.js69
-rw-r--r--app/assets/javascripts/merge_request.js15
-rw-r--r--app/assets/javascripts/merge_request_tabs.js88
-rw-r--r--app/assets/javascripts/merge_request_widget.js8
-rw-r--r--app/assets/javascripts/milestone.js1
-rw-r--r--app/assets/javascripts/milestone_select.js1
-rw-r--r--app/assets/javascripts/network/branch-graph.js13
-rw-r--r--app/assets/javascripts/network/network_bundle.js7
-rw-r--r--app/assets/javascripts/notes.js92
-rw-r--r--app/assets/javascripts/preview_markdown.js10
-rw-r--r--app/assets/javascripts/profile/gl_crop.js12
-rw-r--r--app/assets/javascripts/profile/profile.js4
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js1
-rw-r--r--app/assets/javascripts/project.js6
-rw-r--r--app/assets/javascripts/project_find_file.js31
-rw-r--r--app/assets/javascripts/project_new.js20
-rw-r--r--app/assets/javascripts/project_show.js2
-rw-r--r--app/assets/javascripts/projects_list.js1
-rw-r--r--app/assets/javascripts/protected_branch_dropdown.js.es61
-rw-r--r--app/assets/javascripts/protected_branch_edit.js.es61
-rw-r--r--app/assets/javascripts/search_autocomplete.js59
-rw-r--r--app/assets/javascripts/shortcuts.js1
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js2
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js6
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js3
-rw-r--r--app/assets/javascripts/sidebar.js41
-rw-r--r--app/assets/javascripts/sidebar.js.es695
-rw-r--r--app/assets/javascripts/single_file_diff.js13
-rw-r--r--app/assets/javascripts/snippets_list.js.es611
-rw-r--r--app/assets/javascripts/syntax_highlight.js11
-rw-r--r--app/assets/javascripts/todos.js6
-rw-r--r--app/assets/javascripts/tree.js3
-rw-r--r--app/assets/javascripts/u2f/authenticate.js18
-rw-r--r--app/assets/javascripts/u2f/register.js7
-rw-r--r--app/assets/javascripts/user_tabs.js69
-rw-r--r--app/assets/javascripts/users/calendar.js25
-rw-r--r--app/assets/javascripts/users/users_bundle.js1
-rw-r--r--app/assets/javascripts/users_select.js8
-rw-r--r--app/assets/javascripts/zen_mode.js37
-rw-r--r--app/assets/stylesheets/framework/animations.scss67
-rw-r--r--app/assets/stylesheets/framework/blocks.scss21
-rw-r--r--app/assets/stylesheets/framework/buttons.scss10
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/files.scss33
-rw-r--r--app/assets/stylesheets/framework/filters.scss4
-rw-r--r--app/assets/stylesheets/framework/flash.scss10
-rw-r--r--app/assets/stylesheets/framework/header.scss16
-rw-r--r--app/assets/stylesheets/framework/lists.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss12
-rw-r--r--app/assets/stylesheets/framework/mobile.scss4
-rw-r--r--app/assets/stylesheets/framework/nav.scss5
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss18
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss29
-rw-r--r--app/assets/stylesheets/framework/variables.scss148
-rw-r--r--app/assets/stylesheets/pages/awards.scss7
-rw-r--r--app/assets/stylesheets/pages/boards.scss99
-rw-r--r--app/assets/stylesheets/pages/builds.scss39
-rw-r--r--app/assets/stylesheets/pages/commits.scss12
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss144
-rw-r--r--app/assets/stylesheets/pages/diff.scss9
-rw-r--r--app/assets/stylesheets/pages/editor.scss19
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss13
-rw-r--r--app/assets/stylesheets/pages/issuable.scss17
-rw-r--r--app/assets/stylesheets/pages/issues.scss28
-rw-r--r--app/assets/stylesheets/pages/labels.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss43
-rw-r--r--app/assets/stylesheets/pages/milestone.scss28
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss112
-rw-r--r--app/assets/stylesheets/pages/profile.scss41
-rw-r--r--app/assets/stylesheets/pages/projects.scss82
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/pages/snippets.scss46
-rw-r--r--app/assets/stylesheets/pages/todos.scss2
-rw-r--r--app/assets/stylesheets/pages/tree.scss45
-rw-r--r--app/assets/stylesheets/pages/xterm.scss3
-rw-r--r--app/controllers/admin/groups_controller.rb12
-rw-r--r--app/controllers/admin/projects_controller.rb2
-rw-r--r--app/controllers/ci/lints_controller.rb12
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb1
-rw-r--r--app/controllers/concerns/creates_commit.rb3
-rw-r--r--app/controllers/concerns/issuable_actions.rb35
-rw-r--r--app/controllers/concerns/membership_actions.rb7
-rw-r--r--app/controllers/concerns/service_params.rb2
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb8
-rw-r--r--app/controllers/groups/group_members_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb12
-rw-r--r--app/controllers/import/base_controller.rb17
-rw-r--r--app/controllers/import/bitbucket_controller.rb23
-rw-r--r--app/controllers/import/github_controller.rb18
-rw-r--r--app/controllers/import/gitlab_controller.rb15
-rw-r--r--app/controllers/jwt_controller.rb43
-rw-r--r--app/controllers/profiles_controller.rb3
-rw-r--r--app/controllers/projects/blob_controller.rb10
-rw-r--r--app/controllers/projects/boards/issues_controller.rb2
-rw-r--r--app/controllers/projects/builds_controller.rb12
-rw-r--r--app/controllers/projects/commit_controller.rb17
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb67
-rw-r--r--app/controllers/projects/git_http_client_controller.rb70
-rw-r--r--app/controllers/projects/git_http_controller.rb5
-rw-r--r--app/controllers/projects/hooks_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb39
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb11
-rw-r--r--app/controllers/projects/merge_requests_controller.rb58
-rw-r--r--app/controllers/projects/pipelines_controller.rb9
-rw-r--r--app/controllers/projects/project_members_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb5
-rw-r--r--app/controllers/projects/snippets_controller.rb5
-rw-r--r--app/controllers/projects_controller.rb11
-rw-r--r--app/controllers/search_controller.rb4
-rw-r--r--app/controllers/sent_notifications_controller.rb7
-rw-r--r--app/controllers/snippets_controller.rb3
-rw-r--r--app/controllers/users_controller.rb4
-rw-r--r--app/finders/access_requests_finder.rb27
-rw-r--r--app/finders/issuable_finder.rb24
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/merge_requests_finder.rb10
-rw-r--r--app/finders/pipelines_finder.rb32
-rw-r--r--app/helpers/application_helper.rb28
-rw-r--r--app/helpers/avatars_helper.rb3
-rw-r--r--app/helpers/award_emoji_helper.rb9
-rw-r--r--app/helpers/ci_status_helper.rb2
-rw-r--r--app/helpers/git_helper.rb4
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/helpers/groups_helper.rb25
-rw-r--r--app/helpers/import_helper.rb5
-rw-r--r--app/helpers/issuables_helper.rb36
-rw-r--r--app/helpers/lfs_helper.rb20
-rw-r--r--app/helpers/merge_requests_helper.rb10
-rw-r--r--app/helpers/milestones_helper.rb24
-rw-r--r--app/helpers/namespaces_helper.rb5
-rw-r--r--app/helpers/nav_helper.rb19
-rw-r--r--app/helpers/notes_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb26
-rw-r--r--app/helpers/search_helper.rb35
-rw-r--r--app/helpers/services_helper.rb6
-rw-r--r--app/helpers/sidekiq_helper.rb19
-rw-r--r--app/helpers/snippets_helper.rb6
-rw-r--r--app/helpers/workhorse_helper.rb4
-rw-r--r--app/mailers/notify.rb6
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/blob.rb12
-rw-r--r--app/models/board.rb8
-rw-r--r--app/models/ci/build.rb86
-rw-r--r--app/models/ci/pipeline.rb59
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/ci/variable.rb6
-rw-r--r--app/models/commit_status.rb14
-rw-r--r--app/models/concerns/access_requestable.rb5
-rw-r--r--app/models/concerns/awardable.rb6
-rw-r--r--app/models/concerns/has_status.rb9
-rw-r--r--app/models/concerns/issuable.rb13
-rw-r--r--app/models/cycle_analytics.rb97
-rw-r--r--app/models/cycle_analytics/summary.rb42
-rw-r--r--app/models/deployment.rb34
-rw-r--r--app/models/diff_note.rb18
-rw-r--r--app/models/discussion.rb39
-rw-r--r--app/models/environment.rb16
-rw-r--r--app/models/event.rb27
-rw-r--r--app/models/global_milestone.rb13
-rw-r--r--app/models/group.rb43
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/hooks/web_hook.rb1
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/issue/metrics.rb21
-rw-r--r--app/models/member.rb110
-rw-r--r--app/models/members/group_member.rb16
-rw-r--r--app/models/members/project_member.rb44
-rw-r--r--app/models/merge_request.rb72
-rw-r--r--app/models/merge_request/metrics.rb11
-rw-r--r--app/models/merge_request_diff.rb18
-rw-r--r--app/models/merge_requests_closing_issues.rb7
-rw-r--r--app/models/milestone.rb6
-rw-r--r--app/models/namespace.rb5
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/project.rb50
-rw-r--r--app/models/project_feature.rb6
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb4
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/slack_service.rb72
-rw-r--r--app/models/project_services/slack_service/build_message.rb4
-rw-r--r--app/models/project_services/slack_service/pipeline_message.rb79
-rw-r--r--app/models/project_team.rb86
-rw-r--r--app/models/repository.rb206
-rw-r--r--app/models/service.rb5
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/policies/project_policy.rb19
-rw-r--r--app/services/akismet_service.rb43
-rw-r--r--app/services/auth/container_registry_authentication_service.rb43
-rw-r--r--app/services/ci/process_pipeline_service.rb6
-rw-r--r--app/services/commits/change_service.rb20
-rw-r--r--app/services/commits/cherry_pick_service.rb14
-rw-r--r--app/services/commits/revert_service.rb14
-rw-r--r--app/services/create_deployment_service.rb44
-rw-r--r--app/services/files/base_service.rb2
-rw-r--r--app/services/files/create_dir_service.rb2
-rw-r--r--app/services/files/create_service.rb2
-rw-r--r--app/services/files/delete_service.rb2
-rw-r--r--app/services/files/update_service.rb4
-rw-r--r--app/services/git_push_service.rb10
-rw-r--r--app/services/issuable/bulk_update_service.rb26
-rw-r--r--app/services/issuable_base_service.rb6
-rw-r--r--app/services/issues/base_service.rb7
-rw-r--r--app/services/issues/bulk_update_service.rb25
-rw-r--r--app/services/members/approve_access_request_service.rb31
-rw-r--r--app/services/members/request_access_service.rb25
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/merge_requests/create_service.rb1
-rw-r--r--app/services/merge_requests/post_merge_service.rb1
-rw-r--r--app/services/merge_requests/refresh_service.rb9
-rw-r--r--app/services/merge_requests/update_service.rb4
-rw-r--r--app/services/milestones/create_service.rb2
-rw-r--r--app/services/notes/slash_commands_service.rb21
-rw-r--r--app/services/notification_service.rb19
-rw-r--r--app/services/projects/destroy_service.rb2
-rw-r--r--app/services/projects/housekeeping_service.rb12
-rw-r--r--app/services/projects/import_service.rb5
-rw-r--r--app/services/slash_commands/interpret_service.rb4
-rw-r--r--app/services/system_note_service.rb20
-rw-r--r--app/services/todo_service.rb20
-rw-r--r--app/validators/namespace_validator.rb5
-rw-r--r--app/views/admin/application_settings/_form.html.haml54
-rw-r--r--app/views/admin/background_jobs/_head.html.haml49
-rw-r--r--app/views/admin/background_jobs/show.html.haml8
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml4
-rw-r--r--app/views/admin/builds/_build.html.haml77
-rw-r--r--app/views/admin/builds/index.html.haml43
-rw-r--r--app/views/admin/dashboard/_head.html.haml57
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml6
-rw-r--r--app/views/admin/labels/_form.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml22
-rw-r--r--app/views/admin/runners/show.html.haml12
-rw-r--r--app/views/award_emoji/_awards_block.html.haml2
-rw-r--r--app/views/ci/lints/_create.html.haml9
-rw-r--r--app/views/dashboard/snippets/index.html.haml12
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml2
-rw-r--r--app/views/devise/sessions/two_factor.html.haml3
-rw-r--r--app/views/discussions/_jump_to_next.html.haml2
-rw-r--r--app/views/discussions/_notes.html.haml3
-rw-r--r--app/views/discussions/_resolve_all.html.haml5
-rw-r--r--app/views/doorkeeper/authorized_applications/_delete_form.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml4
-rw-r--r--app/views/explore/snippets/index.html.haml5
-rw-r--r--app/views/groups/_group_lfs_settings.html.haml11
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/groups/group_members/update.js.haml2
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml553
-rw-r--r--app/views/import/base/create.js.haml21
-rw-r--r--app/views/import/base/unauthorized.js.haml14
-rw-r--r--app/views/import/bitbucket/deploy_key.js.haml3
-rw-r--r--app/views/import/bitbucket/status.html.haml2
-rw-r--r--app/views/import/github/status.html.haml12
-rw-r--r--app/views/import/gitlab/status.html.haml2
-rw-r--r--app/views/layouts/_flash.html.haml6
-rw-r--r--app/views/layouts/_page.html.haml3
-rw-r--r--app/views/layouts/_search.html.haml39
-rw-r--r--app/views/layouts/nav/_project.html.haml6
-rw-r--r--app/views/layouts/notify.html.haml4
-rw-r--r--app/views/layouts/project.html.haml3
-rw-r--r--app/views/profiles/keys/index.html.haml1
-rw-r--r--app/views/profiles/show.html.haml3
-rw-r--r--app/views/profiles/update_username.js.haml2
-rw-r--r--app/views/projects/_activity.html.haml19
-rw-r--r--app/views/projects/_last_push.html.haml2
-rw-r--r--app/views/projects/_merge_request_settings.html.haml25
-rw-r--r--app/views/projects/_zen.html.haml3
-rw-r--r--app/views/projects/blame/show.html.haml3
-rw-r--r--app/views/projects/blob/_editor.html.haml7
-rw-r--r--app/views/projects/branches/_branch.html.haml5
-rw-r--r--app/views/projects/builds/_sidebar.html.haml82
-rw-r--r--app/views/projects/builds/_table.html.haml24
-rw-r--r--app/views/projects/builds/index.html.haml47
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml4
-rw-r--r--app/views/projects/ci/builds/_build.html.haml38
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml25
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml31
-rw-r--r--app/views/projects/commit/_builds.html.haml2
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml5
-rw-r--r--app/views/projects/commit/_pipeline.html.haml3
-rw-r--r--app/views/projects/commit/_pipeline_stage.html.haml14
-rw-r--r--app/views/projects/commit/_pipeline_status_group.html.haml11
-rw-r--r--app/views/projects/commit/_pipelines_list.haml12
-rw-r--r--app/views/projects/commit/pipelines.html.haml7
-rw-r--r--app/views/projects/commits/_head.html.haml45
-rw-r--r--app/views/projects/commits/show.html.haml3
-rw-r--r--app/views/projects/compare/_form.html.haml17
-rw-r--r--app/views/projects/compare/_ref_dropdown.html.haml1
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml59
-rw-r--r--app/views/projects/diffs/_content.html.haml4
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/diffs/_file_header.html.haml1
-rw-r--r--app/views/projects/edit.html.haml17
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/forks/index.html.haml4
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml17
-rw-r--r--app/views/projects/graphs/_head.html.haml35
-rw-r--r--app/views/projects/hooks/_project_hook.html.haml2
-rw-r--r--app/views/projects/issues/_head.html.haml57
-rw-r--r--app/views/projects/issues/_issue.html.haml6
-rw-r--r--app/views/projects/issues/_issues.html.haml2
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml19
-rw-r--r--app/views/projects/issues/_related_branches.html.haml2
-rw-r--r--app/views/projects/issues/index.html.haml5
-rw-r--r--app/views/projects/labels/_form.html.haml2
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml4
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml6
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml33
-rw-r--r--app/views/projects/merge_requests/index.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/_versions.html.haml99
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml25
-rw-r--r--app/views/projects/new.html.haml2
-rw-r--r--app/views/projects/notes/_form.html.haml6
-rw-r--r--app/views/projects/notes/_note.html.haml9
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml4
-rw-r--r--app/views/projects/pipelines/_head.html.haml45
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/pipelines/index.html.haml10
-rw-r--r--app/views/projects/project_members/update.js.haml2
-rw-r--r--app/views/projects/protected_branches/_protected_branch.html.haml2
-rw-r--r--app/views/projects/refs/logs_tree.js.haml4
-rw-r--r--app/views/projects/runners/_form.html.haml4
-rw-r--r--app/views/projects/runners/_runner.html.haml2
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml16
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml12
-rw-r--r--app/views/projects/runners/index.html.haml12
-rw-r--r--app/views/projects/snippets/_actions.html.haml22
-rw-r--r--app/views/projects/snippets/index.html.haml7
-rw-r--r--app/views/projects/snippets/show.html.haml12
-rw-r--r--app/views/projects/tree/_blob_item.html.haml6
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--app/views/projects/tree/_tree_content.html.haml14
-rw-r--r--app/views/projects/tree/_tree_item.html.haml6
-rw-r--r--app/views/projects/tree/show.html.haml2
-rw-r--r--app/views/projects/triggers/index.html.haml130
-rw-r--r--app/views/projects/variables/_table.html.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml2
-rw-r--r--app/views/projects/wikis/_nav.html.haml25
-rw-r--r--app/views/search/_results.html.haml16
-rw-r--r--app/views/search/results/_blob.html.haml2
-rw-r--r--app/views/search/results/_commit.html.haml3
-rw-r--r--app/views/search/results/_wiki_blob.html.haml2
-rw-r--r--app/views/sent_notifications/unsubscribe.html.haml19
-rw-r--r--app/views/shared/_milestones_filter.html.haml15
-rw-r--r--app/views/shared/_sort_dropdown.html.haml22
-rw-r--r--app/views/shared/_visibility_level.html.haml4
-rw-r--r--app/views/shared/_visibility_radios.html.haml2
-rw-r--r--app/views/shared/builds/_tabs.html.haml24
-rw-r--r--app/views/shared/icons/_icon_cycle_analytics_splash.svg1
-rw-r--r--app/views/shared/icons/_icon_fork.svg4
-rw-r--r--app/views/shared/icons/_icon_no_wrap.svg3
-rw-r--r--app/views/shared/icons/_icon_soft_wrap.svg3
-rw-r--r--app/views/shared/issuable/_filter.html.haml23
-rw-r--r--app/views/shared/issuable/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml1
-rw-r--r--app/views/shared/issuable/_nav.html.haml20
-rw-r--r--app/views/shared/issuable/_search_form.html.haml4
-rw-r--r--app/views/shared/milestones/_milestone.html.haml2
-rw-r--r--app/views/shared/snippets/_header.html.haml8
-rw-r--r--app/views/shared/snippets/_snippet.html.haml21
-rw-r--r--app/views/shared/web_hooks/_form.html.haml7
-rw-r--r--app/views/snippets/_actions.html.haml22
-rw-r--r--app/views/snippets/_snippets.html.haml18
-rw-r--r--app/views/snippets/show.html.haml21
-rw-r--r--app/views/u2f/_authenticate.html.haml2
-rw-r--r--app/views/users/calendar.html.haml4
-rw-r--r--app/views/users/show.html.haml98
-rw-r--r--app/workers/prune_old_events_worker.rb17
-rw-r--r--changelogs/archive.md1810
-rw-r--r--changelogs/unreleased/.gitkeep0
-rw-r--r--config/application.rb19
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/initializers/7_redis.rb3
-rw-r--r--config/initializers/attr_encrypted_no_db_connection.rb25
-rw-r--r--config/initializers/connection_fix.rb2
-rw-r--r--config/initializers/gitlab_workhorse_secret.rb8
-rw-r--r--config/initializers/metrics.rb3
-rw-r--r--config/initializers/mime_types.rb8
-rw-r--r--config/initializers/postgresql_limit_fix.rb27
-rw-r--r--config/routes.rb22
-rw-r--r--db/fixtures/development/04_project.rb11
-rw-r--r--db/fixtures/development/06_teams.rb2
-rw-r--r--db/fixtures/development/14_pipelines.rb10
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb246
-rw-r--r--db/migrate/20140502125220_migrate_repo_size.rb5
-rw-r--r--db/migrate/20160725104020_merge_request_diff_remove_uniq.rb20
-rw-r--r--db/migrate/20160808085531_add_token_to_build.rb10
-rw-r--r--db/migrate/20160808085602_add_index_for_build_token.rb12
-rw-r--r--db/migrate/20160824124900_add_table_issue_metrics.rb37
-rw-r--r--db/migrate/20160825052008_add_table_merge_request_metrics.rb38
-rw-r--r--db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb15
-rw-r--r--db/migrate/20160830211132_add_confidential_issues_events_to_services.rb15
-rw-r--r--db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb15
-rw-r--r--db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb12
-rw-r--r--db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb39
-rw-r--r--db/migrate/20160907131111_add_environment_type_to_environments.rb9
-rw-r--r--db/migrate/20160913162434_remove_projects_pushes_since_gc.rb19
-rw-r--r--db/migrate/20160913212128_change_artifacts_size_column.rb15
-rw-r--r--db/migrate/20160915042921_create_merge_requests_closing_issues.rb34
-rw-r--r--db/migrate/20160920160832_add_index_to_labels_title.rb11
-rw-r--r--db/migrate/20160926145521_add_organization_to_user.rb12
-rw-r--r--db/schema.rb103
-rw-r--r--doc/README.md5
-rw-r--r--doc/administration/auth/ldap.md6
-rw-r--r--doc/administration/container_registry.md4
-rw-r--r--doc/administration/issue_closing_pattern.md49
-rw-r--r--doc/api/README.md24
-rw-r--r--doc/api/award_emoji.md15
-rw-r--r--doc/api/builds.md30
-rw-r--r--doc/api/ci/builds.md9
-rw-r--r--doc/api/ci/lint.md49
-rw-r--r--doc/api/groups.md18
-rw-r--r--doc/api/members.md4
-rw-r--r--doc/api/notes.md6
-rw-r--r--doc/api/notification_settings.md169
-rw-r--r--doc/api/oauth2.md24
-rw-r--r--doc/api/projects.md28
-rw-r--r--doc/api/repository_files.md12
-rw-r--r--doc/api/settings.md14
-rw-r--r--doc/api/users.md9
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/ci/examples/README.md16
-rw-r--r--doc/ci/quick_start/README.md3
-rw-r--r--doc/ci/ssh_keys/README.md3
-rw-r--r--doc/ci/triggers/README.md4
-rw-r--r--doc/ci/triggers/img/builds_page.pngbin33324 -> 76181 bytes
-rw-r--r--doc/ci/triggers/img/trigger_single_build.pngbin2387 -> 21152 bytes
-rw-r--r--doc/ci/triggers/img/trigger_variables.pngbin4433 -> 9315 bytes
-rw-r--r--doc/ci/triggers/img/triggers_page.pngbin12943 -> 12002 bytes
-rw-r--r--doc/ci/variables/README.md12
-rw-r--r--doc/ci/yaml/README.md134
-rw-r--r--doc/container_registry/README.md6
-rw-r--r--doc/customization/issue_closing.md41
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/frontend.md225
-rw-r--r--doc/development/instrumentation.md15
-rw-r--r--doc/development/migration_style_guide.md30
-rw-r--r--doc/gitlab-basics/add-file.md4
-rw-r--r--doc/gitlab-basics/create-issue.md4
-rw-r--r--doc/install/installation.md10
-rw-r--r--doc/install/requirements.md32
-rw-r--r--doc/intro/README.md8
-rw-r--r--doc/raketasks/backup_restore.md6
-rw-r--r--doc/university/README.md139
-rw-r--r--doc/university/glossary/README.md482
-rw-r--r--doc/university/high-availability/aws/README.md387
-rw-r--r--doc/university/high-availability/aws/img/auto-scaling-det.pngbin0 -> 106157 bytes
-rw-r--r--doc/university/high-availability/aws/img/db-subnet-group.pngbin0 -> 98632 bytes
-rw-r--r--doc/university/high-availability/aws/img/ec-subnet.pngbin0 -> 91922 bytes
-rw-r--r--doc/university/high-availability/aws/img/elastic-file-system.pngbin0 -> 109719 bytes
-rw-r--r--doc/university/high-availability/aws/img/ig-rt.pngbin0 -> 42022 bytes
-rw-r--r--doc/university/high-availability/aws/img/ig.pngbin0 -> 26220 bytes
-rw-r--r--doc/university/high-availability/aws/img/instance_specs.pngbin0 -> 40938 bytes
-rw-r--r--doc/university/high-availability/aws/img/new_vpc.pngbin0 -> 54072 bytes
-rw-r--r--doc/university/high-availability/aws/img/policies.pngbin0 -> 132366 bytes
-rw-r--r--doc/university/high-availability/aws/img/rds-net-opt.pngbin0 -> 54996 bytes
-rw-r--r--doc/university/high-availability/aws/img/rds-sec-group.pngbin0 -> 43950 bytes
-rw-r--r--doc/university/high-availability/aws/img/redis-cluster-det.pngbin0 -> 81524 bytes
-rw-r--r--doc/university/high-availability/aws/img/redis-net.pngbin0 -> 100700 bytes
-rw-r--r--doc/university/high-availability/aws/img/route_table.pngbin0 -> 39611 bytes
-rw-r--r--doc/university/high-availability/aws/img/subnet.pngbin0 -> 56466 bytes
-rw-r--r--doc/university/process/README.md30
-rw-r--r--doc/university/support/README.md188
-rw-r--r--doc/university/training/end-user/README.md420
-rw-r--r--doc/update/8.10-to-8.11.md4
-rw-r--r--doc/update/8.11-to-8.12.md8
-rw-r--r--doc/update/8.12-to-8.13.md201
-rw-r--r--doc/user/markdown.md10
-rw-r--r--doc/user/permissions.md33
-rw-r--r--doc/user/project/builds/artifacts.md32
-rw-r--r--doc/user/project/builds/img/build_latest_artifacts_browser.pngbin0 -> 26617 bytes
-rw-r--r--doc/user/project/cycle_analytics.md114
-rw-r--r--doc/user/project/img/cycle_analytics_landing_page.pngbin0 -> 58203 bytes
-rw-r--r--doc/user/project/issues/automatic_issue_closing.md55
-rw-r--r--doc/user/project/merge_requests.md169
-rw-r--r--doc/user/project/merge_requests/authorization_for_merge_requests.md56
-rw-r--r--doc/user/project/merge_requests/cherry_pick_changes.md52
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_commit.png (renamed from doc/workflow/img/cherry_pick_changes_commit.png)bin304098 -> 304098 bytes
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png (renamed from doc/workflow/img/cherry_pick_changes_commit_modal.png)bin264883 -> 264883 bytes
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_mr.png (renamed from doc/workflow/img/cherry_pick_changes_mr.png)bin212267 -> 212267 bytes
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png (renamed from doc/workflow/img/cherry_pick_changes_mr_modal.png)bin186597 -> 186597 bytes
-rw-r--r--doc/user/project/merge_requests/img/commit_compare.png (renamed from doc/workflow/merge_requests/commit_compare.png)bin65010 -> 65010 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_request_diff.pngbin0 -> 69394 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png (renamed from doc/workflow/merge_when_build_succeeds/enable.png)bin68769 -> 68769 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.pngbin0 -> 11136 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png (renamed from doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png)bin17552 -> 17552 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png (renamed from doc/workflow/merge_when_build_succeeds/status.png)bin82655 -> 82655 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_commit.png (renamed from doc/workflow/img/revert_changes_commit.png)bin233750 -> 233750 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_commit_modal.png (renamed from doc/workflow/img/revert_changes_commit_modal.png)bin205046 -> 205046 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_mr.png (renamed from doc/workflow/img/revert_changes_mr.png)bin241051 -> 241051 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_mr_modal.png (renamed from doc/workflow/img/revert_changes_mr_modal.png)bin211022 -> 211022 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions-compare.pngbin0 -> 68722 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions-dropdown.pngbin0 -> 60587 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions.pngbin0 -> 171413 bytes
-rw-r--r--doc/user/project/merge_requests/img/wip_blocked_accept_button.png (renamed from doc/workflow/wip_merge_requests/blocked_accept_button.png)bin32720 -> 32720 bytes
-rw-r--r--doc/user/project/merge_requests/img/wip_mark_as_wip.png (renamed from doc/workflow/wip_merge_requests/mark_as_wip.png)bin21640 -> 21640 bytes
-rw-r--r--doc/user/project/merge_requests/img/wip_unmark_as_wip.png (renamed from doc/workflow/wip_merge_requests/unmark_as_wip.png)bin16606 -> 16606 bytes
-rw-r--r--doc/user/project/merge_requests/merge_when_build_succeeds.md46
-rw-r--r--doc/user/project/merge_requests/revert_changes.md64
-rw-r--r--doc/user/project/merge_requests/versions.md32
-rw-r--r--doc/user/project/merge_requests/work_in_progress_merge_requests.md17
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md289
-rw-r--r--doc/user/project/repository/img/web_editor_new_branch_dropdown.png (renamed from doc/workflow/img/web_editor_new_branch_dropdown.png)bin20436 -> 20436 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_branch_page.png (renamed from doc/workflow/img/web_editor_new_branch_page.png)bin11245 -> 11245 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_directory_dialog.png (renamed from doc/workflow/img/web_editor_new_directory_dialog.png)bin13339 -> 13339 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_directory_dropdown.png (renamed from doc/workflow/img/web_editor_new_directory_dropdown.png)bin20007 -> 20007 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_file_dropdown.png (renamed from doc/workflow/img/web_editor_new_file_dropdown.png)bin20680 -> 20680 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_file_editor.png (renamed from doc/workflow/img/web_editor_new_file_editor.png)bin66261 -> 66261 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_push_widget.png (renamed from doc/workflow/img/web_editor_new_push_widget.png)bin7076 -> 7076 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_tag_dropdown.png (renamed from doc/workflow/img/web_editor_new_tag_dropdown.png)bin20080 -> 20080 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_tag_page.png (renamed from doc/workflow/img/web_editor_new_tag_page.png)bin36610 -> 36610 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_start_new_merge_request.png (renamed from doc/workflow/img/web_editor_start_new_merge_request.png)bin8596 -> 8596 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_template_dropdown_buttons.pngbin0 -> 14131 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_template_dropdown_first_file.pngbin0 -> 25748 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_template_dropdown_mit_license.pngbin0 -> 85413 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_upload_file_dialog.png (renamed from doc/workflow/img/web_editor_upload_file_dialog.png)bin21502 -> 21502 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_upload_file_dropdown.png (renamed from doc/workflow/img/web_editor_upload_file_dropdown.png)bin20651 -> 20651 bytes
-rw-r--r--doc/user/project/repository/web_editor.md175
-rw-r--r--doc/user/project/settings/import_export.md18
-rw-r--r--doc/workflow/README.md19
-rw-r--r--doc/workflow/authorization_for_merge_requests.md41
-rw-r--r--doc/workflow/cherry_pick_changes.md53
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_importer.pngbin22711 -> 65288 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_new_project_page.pngbin13668 -> 24911 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_select_auth_method.pngbin0 -> 42043 bytes
-rw-r--r--doc/workflow/importing/import_projects_from_github.md129
-rw-r--r--doc/workflow/lfs/lfs_administration.md4
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md8
-rw-r--r--doc/workflow/merge_requests.md91
-rw-r--r--doc/workflow/merge_requests/merge_request_diff.pngbin103239 -> 0 bytes
-rw-r--r--doc/workflow/merge_requests/merge_request_diff_without_whitespace.pngbin71896 -> 0 bytes
-rw-r--r--doc/workflow/merge_requests/versions.pngbin100566 -> 0 bytes
-rw-r--r--doc/workflow/merge_when_build_succeeds.md16
-rw-r--r--doc/workflow/revert_changes.md65
-rw-r--r--doc/workflow/web_editor.md152
-rw-r--r--doc/workflow/wip_merge_requests.md14
-rw-r--r--features/project/snippets.feature2
-rw-r--r--features/steps/admin/settings.rb1
-rw-r--r--features/steps/profile/profile.rb2
-rw-r--r--features/steps/project/issues/issues.rb2
-rw-r--r--features/steps/project/merge_requests.rb6
-rw-r--r--features/steps/project/snippets.rb4
-rw-r--r--lib/api/access_requests.rb13
-rw-r--r--lib/api/api.rb5
-rw-r--r--lib/api/api_guard.rb56
-rw-r--r--lib/api/award_emoji.rb29
-rw-r--r--lib/api/commit_statuses.rb54
-rw-r--r--lib/api/entities.rb44
-rw-r--r--lib/api/files.rb12
-rw-r--r--lib/api/groups.rb24
-rw-r--r--lib/api/helpers.rb27
-rw-r--r--lib/api/internal.rb39
-rw-r--r--lib/api/issues.rb12
-rw-r--r--lib/api/lint.rb21
-rw-r--r--lib/api/members.rb25
-rw-r--r--lib/api/milestones.rb3
-rw-r--r--lib/api/notes.rb8
-rw-r--r--lib/api/notification_settings.rb97
-rw-r--r--lib/api/pipelines.rb5
-rw-r--r--lib/api/projects.rb135
-rw-r--r--lib/api/users.rb6
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb24
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb2
-rw-r--r--lib/banzai/filter/sanitization_filter.rb64
-rw-r--r--lib/banzai/filter/task_list_filter.rb12
-rw-r--r--lib/banzai/filter/wiki_link_filter/rewriter.rb1
-rw-r--r--lib/banzai/reference_parser/base_parser.rb6
-rw-r--r--lib/ci/api/builds.rb8
-rw-r--r--lib/ci/api/entities.rb9
-rw-r--r--lib/ci/api/helpers.rb30
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb31
-rw-r--r--lib/ci/mask_secret.rb10
-rw-r--r--lib/ci/version_info.rb52
-rw-r--r--lib/expand_variables.rb17
-rw-r--r--lib/gitlab/access.rb4
-rw-r--r--lib/gitlab/auth.rb123
-rw-r--r--lib/gitlab/auth/result.rb21
-rw-r--r--lib/gitlab/backend/shell.rb15
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb4
-rw-r--r--lib/gitlab/checks/change_access.rb1
-rw-r--r--lib/gitlab/ci/config.rb2
-rw-r--r--lib/gitlab/ci/config/node/configurable.rb10
-rw-r--r--lib/gitlab/ci/config/node/entry.rb14
-rw-r--r--lib/gitlab/ci/config/node/environment.rb68
-rw-r--r--lib/gitlab/ci/config/node/factory.rb8
-rw-r--r--lib/gitlab/ci/config/node/global.rb14
-rw-r--r--lib/gitlab/ci/config/node/job.rb81
-rw-r--r--lib/gitlab/ci/config/node/jobs.rb28
-rw-r--r--lib/gitlab/ci/config/node/null.rb34
-rw-r--r--lib/gitlab/ci/config/node/undefined.rb27
-rw-r--r--lib/gitlab/ci/config/node/unspecified.rb19
-rw-r--r--lib/gitlab/ci/pipeline_duration.rb141
-rw-r--r--lib/gitlab/conflict/parser.rb2
-rw-r--r--lib/gitlab/contributions_calendar.rb17
-rw-r--r--lib/gitlab/current_settings.rb4
-rw-r--r--lib/gitlab/database/date_time.rb27
-rw-r--r--lib/gitlab/database/median.rb112
-rw-r--r--lib/gitlab/database/migration_helpers.rb10
-rw-r--r--lib/gitlab/git.rb10
-rw-r--r--lib/gitlab/git/hook.rb12
-rw-r--r--lib/gitlab/git_access.rb19
-rw-r--r--lib/gitlab/github_import/base_formatter.rb7
-rw-r--r--lib/gitlab/github_import/client.rb11
-rw-r--r--lib/gitlab/github_import/comment_formatter.rb8
-rw-r--r--lib/gitlab/github_import/importer.rb138
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb10
-rw-r--r--lib/gitlab/github_import/label_formatter.rb6
-rw-r--r--lib/gitlab/github_import/project_creator.rb7
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb10
-rw-r--r--lib/gitlab/github_import/release_formatter.rb23
-rw-r--r--lib/gitlab/gitlab_import/importer.rb5
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/import_export.rb3
-rw-r--r--lib/gitlab/import_export/import_export.yml15
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb8
-rw-r--r--lib/gitlab/import_export/relation_factory.rb28
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb4
-rw-r--r--lib/gitlab/import_export/version_checker.rb4
-rw-r--r--lib/gitlab/ldap/access.rb2
-rw-r--r--lib/gitlab/ldap/adapter.rb65
-rw-r--r--lib/gitlab/lfs_token.rb48
-rw-r--r--lib/gitlab/popen.rb16
-rw-r--r--lib/gitlab/project_search_results.rb5
-rw-r--r--lib/gitlab/redis.rb37
-rw-r--r--lib/gitlab/regex.rb10
-rw-r--r--lib/gitlab/search_results.rb9
-rw-r--r--lib/gitlab/snippet_search_results.rb4
-rw-r--r--lib/gitlab/workhorse.rb56
-rw-r--r--lib/tasks/flog.rake25
-rw-r--r--lib/tasks/haml-lint.rake5
-rw-r--r--public/deploy.html7
-rw-r--r--public/robots.txt2
-rwxr-xr-xscripts/lint-doc.sh9
-rwxr-xr-xscripts/prepare_build.sh2
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb3
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb41
-rw-r--r--spec/controllers/import/github_controller_spec.rb77
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb41
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb3
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb6
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb6
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb2
-rw-r--r--spec/controllers/projects/services_controller_spec.rb16
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb2
-rw-r--r--spec/controllers/projects_controller_spec.rb41
-rw-r--r--spec/controllers/sent_notifications_controller_spec.rb109
-rw-r--r--spec/controllers/sessions_controller_spec.rb23
-rw-r--r--spec/controllers/snippets_controller_spec.rb33
-rw-r--r--spec/factories/ci/runner_projects.rb11
-rw-r--r--spec/factories/ci/runners.rb23
-rw-r--r--spec/factories/ci/variables.rb14
-rw-r--r--spec/factories/deployments.rb3
-rw-r--r--spec/factories/events.rb5
-rw-r--r--spec/factories/group_members.rb19
-rw-r--r--spec/factories/issues.rb4
-rw-r--r--spec/factories/milestones.rb7
-rw-r--r--spec/factories/notes.rb5
-rw-r--r--spec/factories/project_members.rb23
-rw-r--r--spec/factories/projects.rb17
-rw-r--r--spec/features/boards/boards_spec.rb238
-rw-r--r--spec/features/boards/keyboard_shortcut_spec.rb24
-rw-r--r--spec/features/calendar_spec.rb132
-rw-r--r--spec/features/compare_spec.rb26
-rw-r--r--spec/features/dashboard/snippets_spec.rb15
-rw-r--r--spec/features/dashboard_issues_spec.rb3
-rw-r--r--spec/features/environments_spec.rb2
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb47
-rw-r--r--spec/features/issues/filter_by_labels_spec.rb129
-rw-r--r--spec/features/issues/filter_issues_spec.rb64
-rw-r--r--spec/features/issues/reset_filters_spec.rb81
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb79
-rw-r--r--spec/features/issues_spec.rb20
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb3
-rw-r--r--spec/features/merge_requests/merge_request_versions_spec.rb64
-rw-r--r--spec/features/merge_requests/update_merge_requests_spec.rb132
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb10
-rw-r--r--spec/features/milestone_spec.rb21
-rw-r--r--spec/features/profiles/keys_spec.rb18
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb6
-rw-r--r--spec/features/projects/branches_spec.rb48
-rw-r--r--spec/features/projects/builds_spec.rb134
-rw-r--r--spec/features/projects/edit_spec.rb57
-rw-r--r--spec/features/projects/files/download_buttons_spec.rb6
-rw-r--r--spec/features/projects/files/edit_file_soft_wrap_spec.rb41
-rw-r--r--spec/features/projects/gfm_autocomplete_load_spec.rb21
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb80
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin676870 -> 1363770 bytes
-rw-r--r--spec/features/projects/issuable_templates_spec.rb33
-rw-r--r--spec/features/projects/main/download_buttons_spec.rb6
-rw-r--r--spec/features/projects/members/owner_cannot_leave_project_spec.rb4
-rw-r--r--spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb4
-rw-r--r--spec/features/projects/snippets_spec.rb14
-rw-r--r--spec/features/projects/tags/download_buttons_spec.rb6
-rw-r--r--spec/features/projects_spec.rb14
-rw-r--r--spec/features/runners_spec.rb4
-rw-r--r--spec/features/search_spec.rb32
-rw-r--r--spec/features/snippets_spec.rb14
-rw-r--r--spec/features/todos/todos_filtering_spec.rb20
-rw-r--r--spec/features/triggers_spec.rb2
-rw-r--r--spec/features/u2f_spec.rb14
-rw-r--r--spec/features/unsubscribe_links_spec.rb75
-rw-r--r--spec/features/users/snippets_spec.rb18
-rw-r--r--spec/finders/access_requests_finder_spec.rb89
-rw-r--r--spec/finders/issues_finder_spec.rb20
-rw-r--r--spec/finders/joined_groups_finder_spec.rb2
-rw-r--r--spec/finders/pipelines_finder_spec.rb52
-rw-r--r--spec/finders/projects_finder_spec.rb2
-rw-r--r--spec/helpers/git_helper_spec.rb9
-rw-r--r--spec/helpers/groups_helper_spec.rb63
-rw-r--r--spec/helpers/import_helper_spec.rb24
-rw-r--r--spec/helpers/issuables_helper_spec.rb105
-rw-r--r--spec/helpers/members_helper_spec.rb4
-rw-r--r--spec/helpers/milestones_helper_spec.rb33
-rw-r--r--spec/helpers/nav_helper_spec.rb25
-rw-r--r--spec/helpers/projects_helper_spec.rb44
-rw-r--r--spec/helpers/search_helper_spec.rb36
-rw-r--r--spec/helpers/sidekiq_helper_spec.rb40
-rw-r--r--spec/javascripts/awards_handler_spec.js7
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js3
-rw-r--r--spec/javascripts/datetime_utility_spec.js.coffee50
-rw-r--r--spec/javascripts/datetime_utility_spec.js.es664
-rw-r--r--spec/javascripts/fixtures/comments.html.haml21
-rw-r--r--spec/javascripts/fixtures/u2f/authenticate.html.haml2
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js8
-rw-r--r--spec/javascripts/issue_spec.js2
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es613
-rw-r--r--spec/javascripts/new_branch_spec.js2
-rw-r--r--spec/javascripts/notes_spec.js57
-rw-r--r--spec/javascripts/project_title_spec.js12
-rw-r--r--spec/javascripts/right_sidebar_spec.js4
-rw-r--r--spec/javascripts/search_autocomplete_spec.js14
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js1
-rw-r--r--spec/javascripts/spec_helper.js38
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js8
-rw-r--r--spec/javascripts/u2f/register_spec.js8
-rw-r--r--spec/javascripts/zen_mode_spec.js4
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb7
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb56
-rw-r--r--spec/lib/ci/mask_secret_spec.rb27
-rw-r--r--spec/lib/expand_variables_spec.rb73
-rw-r--r--spec/lib/gitlab/auth_spec.rb96
-rw-r--r--spec/lib/gitlab/backend/shell_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/config/node/cache_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/node/environment_spec.rb155
-rw-r--r--spec/lib/gitlab/ci/config/node/factory_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/config/node/global_spec.rb81
-rw-r--r--spec/lib/gitlab/ci/config/node/job_spec.rb88
-rw-r--r--spec/lib/gitlab/ci/config/node/jobs_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/config/node/null_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/config/node/script_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/node/undefined_spec.rb33
-rw-r--r--spec/lib/gitlab/ci/config/node/unspecified_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/pipeline_duration_spec.rb115
-rw-r--r--spec/lib/gitlab/conflict/parser_spec.rb4
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb99
-rw-r--r--spec/lib/gitlab/git_access_spec.rb116
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb9
-rw-r--r--spec/lib/gitlab/git_spec.rb45
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/comment_formatter_spec.rb6
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb57
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb6
-rw-r--r--spec/lib/gitlab/github_import/label_formatter_spec.rb28
-rw-r--r--spec/lib/gitlab/github_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb6
-rw-r--r--spec/lib/gitlab/github_import/release_formatter_spec.rb54
-rw-r--r--spec/lib/gitlab/gitlab_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml187
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb55
-rw-r--r--spec/lib/gitlab/import_export/model_configuration_spec.rb57
-rw-r--r--spec/lib/gitlab/import_export/project.json119
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb40
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb5
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml330
-rw-r--r--spec/lib/gitlab/import_export/version_checker_spec.rb2
-rw-r--r--spec/lib/gitlab/ldap/adapter_spec.rb108
-rw-r--r--spec/lib/gitlab/lfs_token_spec.rb51
-rw-r--r--spec/lib/gitlab/popen_spec.rb9
-rw-r--r--spec/lib/gitlab/redis_spec.rb52
-rw-r--r--spec/lib/gitlab/search_results_spec.rb18
-rw-r--r--spec/lib/gitlab/snippet_search_results_spec.rb6
-rw-r--r--spec/lib/gitlab/template/issue_template_spec.rb6
-rw-r--r--spec/lib/gitlab/template/merge_request_template_spec.rb6
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb131
-rw-r--r--spec/mailers/notify_spec.rb64
-rw-r--r--spec/mailers/shared/notify.rb9
-rw-r--r--spec/models/blob_spec.rb20
-rw-r--r--spec/models/build_spec.rb106
-rw-r--r--spec/models/ci/build_spec.rb62
-rw-r--r--spec/models/ci/pipeline_spec.rb118
-rw-r--r--spec/models/commit_status_spec.rb43
-rw-r--r--spec/models/cycle_analytics/code_spec.rb42
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb50
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb52
-rw-r--r--spec/models/cycle_analytics/production_spec.rb54
-rw-r--r--spec/models/cycle_analytics/review_spec.rb35
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb64
-rw-r--r--spec/models/cycle_analytics/summary_spec.rb59
-rw-r--r--spec/models/cycle_analytics/test_spec.rb94
-rw-r--r--spec/models/diff_note_spec.rb37
-rw-r--r--spec/models/discussion_spec.rb96
-rw-r--r--spec/models/environment_spec.rb16
-rw-r--r--spec/models/event_spec.rb43
-rw-r--r--spec/models/global_milestone_spec.rb5
-rw-r--r--spec/models/group_spec.rb46
-rw-r--r--spec/models/hooks/project_hook_spec.rb18
-rw-r--r--spec/models/hooks/service_hook_spec.rb18
-rw-r--r--spec/models/hooks/system_hook_spec.rb20
-rw-r--r--spec/models/hooks/web_hook_spec.rb18
-rw-r--r--spec/models/issue/metrics_spec.rb55
-rw-r--r--spec/models/issue_spec.rb2
-rw-r--r--spec/models/member_spec.rb287
-rw-r--r--spec/models/members/group_member_spec.rb46
-rw-r--r--spec/models/members/project_member_spec.rb69
-rw-r--r--spec/models/merge_request/metrics_spec.rb18
-rw-r--r--spec/models/merge_request_diff_spec.rb22
-rw-r--r--spec/models/merge_request_spec.rb252
-rw-r--r--spec/models/milestone_spec.rb4
-rw-r--r--spec/models/project_services/asana_service_spec.rb20
-rw-r--r--spec/models/project_services/assembla_service_spec.rb20
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb20
-rw-r--r--spec/models/project_services/bugzilla_service_spec.rb20
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb20
-rw-r--r--spec/models/project_services/campfire_service_spec.rb20
-rw-r--r--spec/models/project_services/custom_issue_tracker_service_spec.rb36
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb20
-rw-r--r--spec/models/project_services/external_wiki_service_spec.rb21
-rw-r--r--spec/models/project_services/flowdock_service_spec.rb20
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb20
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb20
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb20
-rw-r--r--spec/models/project_services/irker_service_spec.rb20
-rw-r--r--spec/models/project_services/jira_service_spec.rb20
-rw-r--r--spec/models/project_services/pivotaltracker_service_spec.rb20
-rw-r--r--spec/models/project_services/pushover_service_spec.rb20
-rw-r--r--spec/models/project_services/redmine_service_spec.rb20
-rw-r--r--spec/models/project_services/slack_service/build_message_spec.rb32
-rw-r--r--spec/models/project_services/slack_service/pipeline_message_spec.rb55
-rw-r--r--spec/models/project_services/slack_service_spec.rb91
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb20
-rw-r--r--spec/models/project_spec.rb210
-rw-r--r--spec/models/project_team_spec.rb66
-rw-r--r--spec/models/repository_spec.rb227
-rw-r--r--spec/models/snippet_spec.rb2
-rw-r--r--spec/policies/project_policy_spec.rb13
-rw-r--r--spec/requests/api/access_requests_spec.rb18
-rw-r--r--spec/requests/api/api_helpers_spec.rb25
-rw-r--r--spec/requests/api/award_emoji_spec.rb54
-rw-r--r--spec/requests/api/builds_spec.rb27
-rw-r--r--spec/requests/api/commit_statuses_spec.rb39
-rw-r--r--spec/requests/api/commits_spec.rb2
-rw-r--r--spec/requests/api/files_spec.rb75
-rw-r--r--spec/requests/api/fork_spec.rb66
-rw-r--r--spec/requests/api/groups_spec.rb11
-rw-r--r--spec/requests/api/internal_spec.rb50
-rw-r--r--spec/requests/api/issues_spec.rb129
-rw-r--r--spec/requests/api/lint_spec.rb49
-rw-r--r--spec/requests/api/members_spec.rb27
-rw-r--r--spec/requests/api/merge_requests_spec.rb4
-rw-r--r--spec/requests/api/milestones_spec.rb31
-rw-r--r--spec/requests/api/notes_spec.rb9
-rw-r--r--spec/requests/api/notification_settings_spec.rb89
-rw-r--r--spec/requests/api/projects_spec.rb27
-rw-r--r--spec/requests/api/settings_spec.rb36
-rw-r--r--spec/requests/api/users_spec.rb9
-rw-r--r--spec/requests/ci/api/builds_spec.rb157
-rw-r--r--spec/requests/git_http_spec.rb631
-rw-r--r--spec/requests/jwt_controller_spec.rb34
-rw-r--r--spec/requests/lfs_http_spec.rb295
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb64
-rw-r--r--spec/services/boards/issues/list_service_spec.rb4
-rw-r--r--spec/services/boards/issues/move_service_spec.rb4
-rw-r--r--spec/services/boards/lists/create_service_spec.rb8
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb8
-rw-r--r--spec/services/boards/lists/move_service_spec.rb4
-rw-r--r--spec/services/create_deployment_service_spec.rb122
-rw-r--r--spec/services/git_push_service_spec.rb52
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb (renamed from spec/services/issues/bulk_update_service_spec.rb)6
-rw-r--r--spec/services/issues/close_service_spec.rb40
-rw-r--r--spec/services/issues/create_service_spec.rb54
-rw-r--r--spec/services/issues/reopen_service_spec.rb46
-rw-r--r--spec/services/issues/update_service_spec.rb170
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb96
-rw-r--r--spec/services/members/request_access_service_spec.rb57
-rw-r--r--spec/services/merge_requests/build_service_spec.rb4
-rw-r--r--spec/services/merge_requests/create_service_spec.rb29
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb24
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb64
-rw-r--r--spec/services/merge_requests/update_service_spec.rb37
-rw-r--r--spec/services/notes/slash_commands_service_spec.rb69
-rw-r--r--spec/services/notification_service_spec.rb16
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb33
-rw-r--r--spec/services/projects/import_service_spec.rb10
-rw-r--r--spec/services/protected_branches/create_service_spec.rb23
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb71
-rw-r--r--spec/services/system_note_service_spec.rb6
-rw-r--r--spec/services/todo_service_spec.rb16
-rw-r--r--spec/spec_helper.rb2
-rw-r--r--spec/support/cycle_analytics_helpers.rb68
-rw-r--r--spec/support/cycle_analytics_helpers/test_generation.rb161
-rw-r--r--spec/support/db_cleaner.rb2
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb (renamed from spec/support/issuable_slash_commands_shared_examples.rb)68
-rw-r--r--spec/support/git_helpers.rb9
-rw-r--r--spec/support/git_http_helpers.rb48
-rw-r--r--spec/support/import_export/configuration_helper.rb29
-rw-r--r--spec/support/import_export/export_file_helper.rb133
-rw-r--r--spec/support/ldap_helpers.rb47
-rw-r--r--spec/support/login_helpers.rb1
-rw-r--r--spec/support/matchers/have_issuable_counts.rb21
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb (renamed from spec/support/issuable_create_service_slash_commands_shared_examples.rb)0
-rw-r--r--spec/support/slash_commands_helpers.rb10
-rw-r--r--spec/support/snippets_shared_examples.rb18
-rw-r--r--spec/support/wait_for_vue_resource.rb7
-rw-r--r--spec/support/workhorse_helpers.rb5
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb2
-rw-r--r--spec/views/ci/lints/show.html.haml_spec.rb51
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb14
-rw-r--r--spec/views/projects/issues/_related_branches.html.haml_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/_heading.html.haml_spec.rb4
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb15
-rw-r--r--spec/views/projects/notes/_form.html.haml_spec.rb36
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb53
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb2
-rw-r--r--spec/workers/prune_old_events_worker_spec.rb24
-rw-r--r--vendor/assets/javascripts/task_list.js161
-rw-r--r--vendor/gitignore/Erlang.gitignore2
-rw-r--r--vendor/gitignore/Global/Ansible.gitignore1
-rw-r--r--vendor/gitignore/Global/Linux.gitignore3
-rw-r--r--vendor/gitignore/Global/NetBeans.gitignore1
-rw-r--r--vendor/gitignore/Global/Tags.gitignore1
-rw-r--r--vendor/gitignore/Global/macOS.gitignore (renamed from vendor/gitignore/Global/OSX.gitignore)3
-rw-r--r--vendor/gitignore/Go.gitignore3
-rw-r--r--vendor/gitignore/Haskell.gitignore1
-rw-r--r--vendor/gitignore/Joomla.gitignore16
-rw-r--r--vendor/gitignore/Node.gitignore6
-rw-r--r--vendor/gitignore/Objective-C.gitignore2
-rw-r--r--vendor/gitignore/Python.gitignore1
-rw-r--r--vendor/gitignore/Rails.gitignore6
-rw-r--r--vendor/gitignore/TeX.gitignore3
-rw-r--r--vendor/gitignore/VisualStudio.gitignore15
-rw-r--r--vendor/gitlab-ci-yml/.gitlab-ci.yml4
-rw-r--r--vendor/gitlab-ci-yml/Docker.gitlab-ci.yml7
-rw-r--r--vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml34
-rw-r--r--vendor/gitlab-ci-yml/Julia.gitlab-ci.yml54
-rw-r--r--vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml9
-rw-r--r--vendor/gitlab-ci-yml/Swift.gitlab-ci.yml30
1026 files changed, 24747 insertions, 8393 deletions
diff --git a/.flayignore b/.flayignore
index 9c9875d4f9e..44df2ba2371 100644
--- a/.flayignore
+++ b/.flayignore
@@ -1 +1,3 @@
*.erb
+lib/gitlab/sanitizers/svg/whitelist.rb
+lib/gitlab/diff/position_tracer.rb
diff --git a/.gitignore b/.gitignore
index 1bf9a47aef6..9166512606d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,3 +48,4 @@
/vendor/bundle/*
/builds/*
/shared/*
+/.gitlab_workhorse_secret
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index be5614520a5..a11c4705e82 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -12,7 +12,7 @@ variables:
RSPEC_RETRY_RETRY_COUNT: "3"
RAILS_ENV: "test"
SIMPLECOV: "true"
- USE_DB: "true"
+ SETUP_DB: "true"
USE_BUNDLE_INSTALL: "true"
GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1"
@@ -23,7 +23,7 @@ before_script:
- bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- retry gem install knapsack
- - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate'
+ - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate'
stages:
- prepare
@@ -35,7 +35,7 @@ stages:
.knapsack-state: &knapsack-state
services: []
variables:
- USE_DB: "false"
+ SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
cache:
key: "knapsack"
@@ -82,7 +82,7 @@ update-knapsack:
- export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
- - knapsack rspec
+ - knapsack rspec "--color --format documentation"
artifacts:
expire_in: 31d
paths:
@@ -196,7 +196,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
.ruby-static-analysis: &ruby-static-analysis
variables:
SIMPLECOV: "false"
- USE_DB: "false"
+ SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
.exec: &exec
@@ -206,10 +206,12 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
- bundle exec $CI_BUILD_NAME
rubocop: *exec
+rake haml_lint: *exec
rake scss_lint: *exec
rake brakeman: *exec
-rake flog: *exec
-rake flay: *exec
+rake flay:
+ <<: *exec
+ allow_failure: yes
license_finder: *exec
rake downtime_check: *exec
@@ -219,6 +221,23 @@ rake db:migrate:reset:
script:
- rake db:migrate:reset
+rake db:seed_fu:
+ stage: test
+ <<: *use-db
+ variables:
+ SIZE: "1"
+ SETUP_DB: "false"
+ RAILS_ENV: "development"
+ script:
+ - git clone https://gitlab.com/gitlab-org/gitlab-test.git
+ /home/git/repositories/gitlab-org/gitlab-test.git
+ - bundle exec rake db:setup db:seed_fu
+ artifacts:
+ when: on_failure
+ expire_in: 1d
+ paths:
+ - log/development.log
+
teaspoon:
stage: test
<<: *use-db
@@ -248,11 +267,26 @@ bundler:audit:
script:
- "bundle exec bundle-audit check --update --ignore OSVDB-115941"
+migration paths:
+ stage: test
+ <<: *use-db
+ only:
+ - master@gitlab-org/gitlab-ce
+ script:
+ - git checkout HEAD .
+ - git fetch --tags
+ - git checkout v8.5.9
+ - 'echo test: unix:/var/opt/gitlab/redis/redis.socket > config/resque.yml'
+ - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" --retry=3
+ - rake db:drop db:create db:schema:load db:seed_fu
+ - git checkout $CI_BUILD_REF
+ - rake db:migrate
+
coverage:
stage: post-test
services: []
variables:
- USE_DB: "false"
+ SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
script:
- bundle exec scripts/merge-simplecov
@@ -263,13 +297,12 @@ coverage:
- coverage/index.html
- coverage/assets/
-
# Notify slack in the end
notify:slack:
stage: post-test
variables:
- USE_DB: "false"
+ SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index b676916fdf4..ac38f0c9521 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -10,7 +10,7 @@
(What you should see instead)
-### Actual behaviour
+### Actual behavior
(What actually happens)
diff --git a/.haml-lint.yml b/.haml-lint.yml
new file mode 100644
index 00000000000..da9a43d9c6d
--- /dev/null
+++ b/.haml-lint.yml
@@ -0,0 +1,103 @@
+# Whether to ignore frontmatter at the beginning of HAML documents for
+# frameworks such as Jekyll/Middleman
+skip_frontmatter: false
+exclude:
+ - 'vendor/**/*'
+ - 'spec/**/*'
+
+linters:
+ AltText:
+ enabled: false
+
+ ClassAttributeWithStaticValue:
+ enabled: false
+
+ ClassesBeforeIds:
+ enabled: false
+
+ ConsecutiveComments:
+ enabled: false
+
+ ConsecutiveSilentScripts:
+ enabled: false
+ max_consecutive: 2
+
+ EmptyObjectReference:
+ enabled: true
+
+ EmptyScript:
+ enabled: true
+
+ FinalNewline:
+ enabled: false
+ present: true
+
+ HtmlAttributes:
+ enabled: false
+
+ ImplicitDiv:
+ enabled: false
+
+ LeadingCommentSpace:
+ enabled: false
+
+ LineLength:
+ enabled: false
+ max: 80
+
+ MultilinePipe:
+ enabled: false
+
+ MultilineScript:
+ enabled: true
+
+ ObjectReferenceAttributes:
+ enabled: true
+
+ RuboCop:
+ enabled: false
+ # These cops are incredibly noisy when it comes to HAML templates, so we
+ # ignore them.
+ ignored_cops:
+ - Lint/BlockAlignment
+ - Lint/EndAlignment
+ - Lint/Void
+ - Metrics/LineLength
+ - Style/AlignParameters
+ - Style/BlockNesting
+ - Style/ElseAlignment
+ - Style/FileName
+ - Style/FinalNewline
+ - Style/FrozenStringLiteralComment
+ - Style/IfUnlessModifier
+ - Style/IndentationWidth
+ - Style/Next
+ - Style/TrailingBlankLines
+ - Style/TrailingWhitespace
+ - Style/WhileUntilModifier
+
+ RubyComments:
+ enabled: false
+
+ SpaceBeforeScript:
+ enabled: false
+
+ SpaceInsideHashAttributes:
+ enabled: false
+ style: space
+
+ Indentation:
+ enabled: true
+ character: space # or tab
+
+ TagName:
+ enabled: true
+
+ TrailingWhitespace:
+ enabled: false
+
+ UnnecessaryInterpolation:
+ enabled: false
+
+ UnnecessaryStringOutput:
+ enabled: false
diff --git a/.pkgr.yml b/.pkgr.yml
index 8fc9fddf8f7..10bcd7bd4bd 100644
--- a/.pkgr.yml
+++ b/.pkgr.yml
@@ -3,6 +3,8 @@ group: git
services:
- postgres
before_precompile: ./bin/pkgr_before_precompile.sh
+env:
+ - SKIP_STORAGE_VALIDATION=true
targets:
debian-7: &wheezy
build_dependencies:
@@ -25,6 +27,16 @@ targets:
- libicu52
- libpcre3
- git
+ ubuntu-16.04:
+ build_dependencies:
+ - libkrb5-dev
+ - libicu-dev
+ - cmake
+ - pkg-config
+ dependencies:
+ - libicu55
+ - libpcre3
+ - git
centos-6:
build_dependencies:
- krb5-devel
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 20daf1619a7..87520c67dd5 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,21 +1,21 @@
# This configuration was generated by
# `rubocop --auto-gen-config --exclude-limit 0`
-# on 2016-07-13 12:36:08 -0600 using RuboCop version 0.41.2.
+# on 2016-09-14 15:44:53 -0400 using RuboCop version 0.42.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
-# Offense count: 154
+# Offense count: 158
Lint/AmbiguousRegexpLiteral:
Enabled: false
-# Offense count: 43
+# Offense count: 41
# Configuration parameters: AllowSafeAssignment.
Lint/AssignmentInCondition:
Enabled: false
-# Offense count: 14
+# Offense count: 16
Lint/HandleExceptions:
Enabled: false
@@ -23,28 +23,28 @@ Lint/HandleExceptions:
Lint/Loop:
Enabled: false
-# Offense count: 15
+# Offense count: 16
Lint/ShadowingOuterLocalVariable:
Enabled: false
-# Offense count: 3
+# Offense count: 6
# Cop supports --auto-correct.
Lint/StringConversionInInterpolation:
Enabled: false
-# Offense count: 44
+# Offense count: 49
# Cop supports --auto-correct.
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument:
Enabled: false
-# Offense count: 129
+# Offense count: 144
# Cop supports --auto-correct.
# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
Lint/UnusedMethodArgument:
Enabled: false
-# Offense count: 12
+# Offense count: 9
# Cop supports --auto-correct.
Performance/PushSplat:
Enabled: false
@@ -59,51 +59,51 @@ Performance/RedundantBlockCall:
Performance/RedundantMatch:
Enabled: false
-# Offense count: 24
+# Offense count: 27
# Cop supports --auto-correct.
# Configuration parameters: MaxKeyValuePairs.
Performance/RedundantMerge:
Enabled: false
-# Offense count: 60
+# Offense count: 61
Rails/OutputSafety:
Enabled: false
-# Offense count: 128
+# Offense count: 129
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: strict, flexible
Rails/TimeZone:
Enabled: false
-# Offense count: 12
+# Offense count: 15
# Cop supports --auto-correct.
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/Validation:
Enabled: false
-# Offense count: 217
+# Offense count: 273
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: with_first_parameter, with_fixed_indentation
Style/AlignParameters:
Enabled: false
-# Offense count: 32
+# Offense count: 30
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: always, conditionals
Style/AndOr:
Enabled: false
-# Offense count: 47
+# Offense count: 50
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: percent_q, bare_percent
Style/BarePercentLiterals:
Enabled: false
-# Offense count: 258
+# Offense count: 289
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: braces, no_braces, context_dependent
@@ -126,14 +126,14 @@ Style/ColonMethodCall:
Style/CommentAnnotation:
Enabled: false
-# Offense count: 34
+# Offense count: 33
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Enabled: false
-# Offense count: 789
+# Offense count: 881
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: leading, trailing
@@ -144,11 +144,12 @@ Style/DotPosition:
Style/DoubleNegation:
Enabled: false
-# Offense count: 3
+# Offense count: 4
+# Cop supports --auto-correct.
Style/EachWithObject:
Enabled: false
-# Offense count: 30
+# Offense count: 25
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: empty, nil, both
@@ -160,7 +161,7 @@ Style/EmptyElse:
Style/EmptyLiteral:
Enabled: false
-# Offense count: 123
+# Offense count: 135
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Style/ExtraSpacing:
@@ -172,16 +173,16 @@ Style/ExtraSpacing:
Style/FormatString:
Enabled: false
-# Offense count: 48
+# Offense count: 51
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
-# Offense count: 11
+# Offense count: 9
Style/IfInsideElse:
Enabled: false
-# Offense count: 177
+# Offense count: 174
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/IfUnlessModifier:
@@ -194,7 +195,7 @@ Style/IfUnlessModifier:
Style/IndentArray:
Enabled: false
-# Offense count: 89
+# Offense count: 97
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_braces
@@ -208,7 +209,7 @@ Style/IndentHash:
Style/Lambda:
Enabled: false
-# Offense count: 6
+# Offense count: 5
# Cop supports --auto-correct.
Style/LineEndConcatenation:
Enabled: false
@@ -218,17 +219,21 @@ Style/LineEndConcatenation:
Style/MethodCallParentheses:
Enabled: false
-# Offense count: 62
+# Offense count: 8
+Style/MethodMissing:
+ Enabled: false
+
+# Offense count: 85
# Cop supports --auto-correct.
Style/MutableConstant:
Enabled: false
-# Offense count: 10
+# Offense count: 8
# Cop supports --auto-correct.
Style/NestedParenthesizedCalls:
Enabled: false
-# Offense count: 12
+# Offense count: 13
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
# SupportedStyles: skip_modifier_ifs, always
@@ -242,12 +247,19 @@ Style/Next:
Style/NumericLiteralPrefix:
Enabled: false
+# Offense count: 64
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: predicate, comparison
+Style/NumericPredicate:
+ Enabled: false
+
# Offense count: 29
# Cop supports --auto-correct.
Style/ParallelAssignment:
Enabled: false
-# Offense count: 208
+# Offense count: 264
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
@@ -265,7 +277,7 @@ Style/PercentQLiterals:
Style/PerlBackrefs:
Enabled: false
-# Offense count: 32
+# Offense count: 35
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_
@@ -273,7 +285,7 @@ Style/PerlBackrefs:
Style/PredicateName:
Enabled: false
-# Offense count: 28
+# Offense count: 27
# Cop supports --auto-correct.
Style/PreferredHashMethods:
Enabled: false
@@ -283,14 +295,14 @@ Style/PreferredHashMethods:
Style/Proc:
Enabled: false
-# Offense count: 20
+# Offense count: 22
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: compact, exploded
Style/RaiseArgs:
Enabled: false
-# Offense count: 3
+# Offense count: 4
# Cop supports --auto-correct.
Style/RedundantBegin:
Enabled: false
@@ -300,29 +312,29 @@ Style/RedundantBegin:
Style/RedundantException:
Enabled: false
-# Offense count: 23
+# Offense count: 24
# Cop supports --auto-correct.
Style/RedundantFreeze:
Enabled: false
-# Offense count: 377
+# Offense count: 408
# Cop supports --auto-correct.
Style/RedundantSelf:
Enabled: false
-# Offense count: 94
+# Offense count: 93
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
Enabled: false
-# Offense count: 17
+# Offense count: 18
# Cop supports --auto-correct.
Style/RescueModifier:
Enabled: false
-# Offense count: 2
+# Offense count: 5
# Cop supports --auto-correct.
Style/SelfAssignment:
Enabled: false
@@ -339,42 +351,42 @@ Style/SingleLineBlockParams:
Style/SingleLineMethods:
Enabled: false
-# Offense count: 119
+# Offense count: 124
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: space, no_space
Style/SpaceBeforeBlockBraces:
Enabled: false
-# Offense count: 11
+# Offense count: 10
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
Style/SpaceBeforeFirstArg:
Enabled: false
-# Offense count: 130
+# Offense count: 141
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space
Style/SpaceInsideBlockBraces:
Enabled: false
-# Offense count: 98
+# Offense count: 96
# Cop supports --auto-correct.
Style/SpaceInsideBrackets:
Enabled: false
-# Offense count: 60
+# Offense count: 62
# Cop supports --auto-correct.
Style/SpaceInsideParens:
Enabled: false
-# Offense count: 5
+# Offense count: 7
# Cop supports --auto-correct.
Style/SpaceInsidePercentLiteralDelimiters:
Enabled: false
-# Offense count: 36
+# Offense count: 40
# Cop supports --auto-correct.
# Configuration parameters: SupportedStyles.
# SupportedStyles: use_perl_names, use_english_names
@@ -388,21 +400,28 @@ Style/SpecialGlobalVars:
Style/StringLiteralsInInterpolation:
Enabled: false
-# Offense count: 24
+# Offense count: 32
# Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method
Style/SymbolProc:
Enabled: false
-# Offense count: 23
+# Offense count: 5
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment.
+# SupportedStyles: require_parentheses, require_no_parentheses
+Style/TernaryParentheses:
+ Enabled: false
+
+# Offense count: 24
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
# SupportedStyles: comma, consistent_comma, no_comma
Style/TrailingCommaInArguments:
Enabled: false
-# Offense count: 113
+# Offense count: 102
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
# SupportedStyles: comma, consistent_comma, no_comma
@@ -415,7 +434,7 @@ Style/TrailingCommaInLiteral:
Style/TrailingUnderscoreVariable:
Enabled: false
-# Offense count: 90
+# Offense count: 76
# Cop supports --auto-correct.
Style/TrailingWhitespace:
Enabled: false
@@ -427,12 +446,12 @@ Style/TrailingWhitespace:
Style/TrivialAccessors:
Enabled: false
-# Offense count: 3
+# Offense count: 2
# Cop supports --auto-correct.
Style/UnlessElse:
Enabled: false
-# Offense count: 13
+# Offense count: 14
# Cop supports --auto-correct.
Style/UnneededInterpolation:
Enabled: false
diff --git a/CHANGELOG b/CHANGELOG
index 934dabe743a..a52ac53bae7 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,79 +1,285 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.12.0 (unreleased)
+v 8.13.0 (unreleased)
+ - Add link from system note to compare with previous version
+ - Use gitlab-shell v3.6.2 (GIT TRACE logging)
+ - Fix centering of custom header logos (Ashley Dumaine)
+ - AbstractReferenceFilter caches project_refs on RequestStore when active
+ - Replaced the check sign to arrow in the show build view. !6501
+ - Speed-up group milestones show page
+ - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
+ - Add more tests for calendar contribution (ClemMakesApps)
+ - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
+ - Fix permission for setting an issue's due date
+ - Expose expires_at field when sharing project on API
+ - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
+ - Allow the Koding integration to be configured through the API
+ - Added soft wrap button to repository file/blob editor
+ - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
+ - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
+ - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
+ - Use a ConnectionPool for Rails.cache on Sidekiq servers
+ - Replace `alias_method_chain` with `Module#prepend`
+ - Preserve label filters when sorting !6136 (Joseph Frazier)
+ - Only update issuable labels if they have been changed
+ - Take filters in account in issuable counters. !6496
+ - Revoke button in Applications Settings underlines on hover.
+ - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
+ - Fix Long commit messages overflow viewport in file tree
+ - Revert avoid touching file system on Build#artifacts?
+ - Add broadcast messages and alerts below sub-nav
+ - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
+ - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
+ - Add organization field to user profile
+ - Fix resolved discussion display in side-by-side diff view !6575
+ - Optimize GitHub importing for speed and memory
+ - API: expose pipeline data in builds API (!6502, Guilherme Salazar)
+ - Notify the Merger about merge after successful build (Dimitris Karakasilis)
+ - Fix broken repository 500 errors in project list
+ - Close todos when accepting merge requests via the API !6486 (tonygambone)
+
+v 8.12.4 (unreleased)
+
+v 8.12.3
+ - Update Gitlab Shell to support low IO priority for storage moves
+
+v 8.12.2 (unreleased)
+ - Fix Import/Export not recognising correctly the imported services.
+ - Fix snippets pagination
+ - Fix "Create project" button layout when visibility options are restricted
+ - Fix List-Unsubscribe header in emails
+ - Fix IssuesController#show degradation including project on loaded notes
+ - Fix an issue with the "Commits" section of the cycle analytics summary. !6513
+ - Fix errors importing project feature and milestone models using GitLab project import
+ - Make JWT messages Docker-compatible
+ - Fix duplicate branch entry in the merge request version compare dropdown
+ - Respect the fork_project permission when forking projects
+ - Only update issuable labels if they have been changed
+ - Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
+ - Fix resolve discussion buttons endpoint path
+
+v 8.12.1
+ - Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
+ - Fix issue with search filter labels not displaying
+
+v 8.12.0
+ - Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251
+ - Only check :can_resolve permission if the note is resolvable
+ - Bump fog-aws to v0.11.0 to support ap-south-1 region
+ - Add ability to fork to a specific namespace using API. (ritave)
+ - Allow to set request_access_enabled for groups and projects
+ - Cleanup misalignments in Issue list view !6206
+ - Only create a protected branch upon a push to a new branch if a rule for that branch doesn't exist
+ - Add Pipelines for Commit
+ - Prune events older than 12 months. (ritave)
+ - Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
+ - Fix issues/merge-request templates dropdown for forked projects
- Filter tags by name !6121
+ - Update gitlab shell secret file also when it is empty. !3774 (glensc)
+ - Give project selection dropdowns responsive width, make non-wrapping.
+ - Fix note form hint showing slash commands supported for commits.
- Make push events have equal vertical spacing.
+ - API: Ensure invitees are not returned in Members API.
+ - Preserve applied filters on issues search.
- Add two-factor recovery endpoint to internal API !5510
+ - Pass the "Remember me" value to the U2F authentication form
+ - Display stages in valid order in stages dropdown on build page
+ - Only update projects.last_activity_at once per hour when creating a new event
+ - Cycle analytics (first iteration) !5986
- Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
+ - Move pushes_since_gc from the database to Redis
+ - Limit number of shown environments on Merge Request: show only environments for target_branch, source_branch and tags
- Add font color contrast to external label in admin area (ClemMakesApps)
+ - Fix find file navigation links (ClemMakesApps)
- Change logo animation to CSS (ClemMakesApps)
- Instructions for enabling Git packfile bitmaps !6104
+ - Use Search::GlobalService.new in the `GET /projects/search/:query` endpoint
+ - Fix long comments in diffs messing with table width
+ - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
+ - Fix pagination on user snippets page
+ - Run CI builds with the permissions of users !5735
+ - Fix sorting of issues in API
+ - Fix download artifacts button links !6407
+ - Sort project variables by key. !6275 (Diego Souza)
+ - Ensure specs on sorting of issues in API are deterministic on MySQL
+ - Added ability to use predefined CI variables for environment name
+ - Added ability to specify URL in environment configuration in gitlab-ci.yml
+ - Escape search term before passing it to Regexp.new !6241 (winniehell)
+ - Fix pinned sidebar behavior in smaller viewports !6169
+ - Fix file permissions change when updating a file on the Gitlab UI !5979
+ - Added horizontal padding on build page sidebar on code coverage block. !6196 (Vitaly Baev)
- Change merge_error column from string to text type
+ - Fix issue with search filter labels not displaying
- Reduce contributions calendar data payload (ClemMakesApps)
+ - Show all pipelines for merge requests even from discarded commits !6414
+ - Replace contributions calendar timezone payload with dates (ClemMakesApps)
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
+ - Enable pipeline events by default !6278
+ - Move parsing of sidekiq ps into helper !6245 (pascalbetz)
+ - Added go to issue boards keyboard shortcut
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
+ - Emoji can be awarded on Snippets !4456
- Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
+ - Fix blame table layout width
+ - Spec testing if issue authors can read issues on private projects
- Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps)
+ - Request only the LDAP attributes we need !6187
- Center build stage columns in pipeline overview (ClemMakesApps)
+ - Fix bug with tooltip not hiding on discussion toggle button
+ - Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps)
+ - Fix bug stopping issue description being scrollable after selecting issue template
+ - Remove suggested colors hover underline (ClemMakesApps)
+ - Fix jump to discussion button being displayed on commit notes
- Shorten task status phrase (ClemMakesApps)
+ - Fix project visibility level fields on settings
- Add hover color to emoji icon (ClemMakesApps)
+ - Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
+ - Add textarea autoresize after comment (ClemMakesApps)
+ - Do not write SSH public key 'comments' to authorized_keys !6381
+ - Refresh todos count cache when an Issue/MR is deleted
- Fix branches page dropdown sort alignment (ClemMakesApps)
+ - Hides merge request button on branches page is user doesn't have permissions
- Add white background for no readme container (ClemMakesApps)
- API: Expose issue confidentiality flag. (Robert Schilling)
+ - Fix markdown anchor icon interaction (ClemMakesApps)
+ - Test migration paths from 8.5 until current release !4874
+ - Replace animateEmoji timeout with eventListener (ClemMakesApps)
+ - Show badges in Milestone tabs. !5946 (Dan Rowden)
- Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
+ - Require confirmation when not logged in for unsubscribe links !6223 (Maximiliano Perez Coto)
- Add `wiki_page_events` to project hook APIs (Ben Boeckel)
- Remove Gitorious import
+ - Loads GFM autocomplete source only when required
+ - Fix issue with slash commands not loading on new issue page
- Fix inconsistent background color for filter input field (ClemMakesApps)
- Remove prefixes from transition CSS property (ClemMakesApps)
- Add Sentry logging to API calls
- Add BroadcastMessage API
+ - Use 'git update-ref' for safer web commits !6130
+ - Sort pipelines requested through the API
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
+ - Fix issue boards loading on large screens
+ - Change pipeline duration to be jobs running time instead of simple wall time from start to end !6084
+ - Show queued time when showing a pipeline !6084
- Remove unused mixins (ClemMakesApps)
+ - Fix issue board label filtering appending already filtered labels
- Add search to all issue board lists
+ - Scroll active tab into view on mobile
- Fix groups sort dropdown alignment (ClemMakesApps)
- Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps)
- Use JavaScript tooltips for mentions !5301 (winniehell)
+ - Add hover state to todos !5361 (winniehell)
+ - Fix icon alignment of star and fork buttons !5451 (winniehell)
+ - Fix alignment of icon buttons !5887 (winniehell)
+ - Added Ubuntu 16.04 support for packager.io (JonTheNiceGuy)
- Fix markdown help references (ClemMakesApps)
- Add last commit time to repo view (ClemMakesApps)
- Fix accessibility and visibility of project list dropdown button !6140
- - Added project specific enable/disable setting for LFS !5997
+ - Fix missing flash messages on service edit page (airatshigapov)
+ - Added project-specific enable/disable setting for LFS !5997
+ - Added group-specific enable/disable setting for LFS !6164
+ - Add optional 'author' param when making commits. !5822 (dandunckelman)
- Don't expose a user's token in the `/api/v3/user` API (!6047)
- Remove redundant js-timeago-pending from user activity log (ClemMakesApps)
- Ability to manage project issues, snippets, wiki, merge requests and builds access level
- Remove inconsistent font weight for sidebar's labels (ClemMakesApps)
+ - Align add button on repository view (ClemMakesApps)
+ - Fix contributions calendar month label truncation (ClemMakesApps)
+ - Import release note descriptions from GitHub (EspadaV8)
- Added tests for diff notes
+ - Add pipeline events to Slack integration !5525
- Add a button to download latest successful artifacts for branches and tags !5142
- Remove redundant pipeline tooltips (ClemMakesApps)
- Expire commit info views after one day, instead of two weeks, to allow for user email updates
- Add delimiter to project stars and forks count (ClemMakesApps)
- Fix badge count alignment (ClemMakesApps)
+ - Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell)
- Fix repo title alignment (ClemMakesApps)
+ - Change update interval of contacted_at
+ - Add LFS support to SSH !6043
- Fix branch title trailing space on hover (ClemMakesApps)
+ - Don't include 'Created By' tag line when importing from GitHub if there is a linked GitLab account (EspadaV8)
- Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
- Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
- Order award emoji tooltips in order they were added (EspadaV8)
- Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps)
- Update merge_requests.md with a simpler way to check out a merge request. !5944
- Fix button missing type (ClemMakesApps)
+ - Gitlab::Checks is now instrumented
- Move to project dropdown with infinite scroll for better performance
- Fix leaking of submit buttons outside the width of a main container !18731 (originally by @pavelloz)
- Load branches asynchronously in Cherry Pick and Revert dialogs.
+ - Convert datetime coffeescript spec to ES6 (ClemMakesApps)
- Add merge request versions !5467
- Change using size to use count and caching it for number of group members. !5935
- Replace play icon font with svg (ClemMakesApps)
- Added 'only_allow_merge_if_build_succeeds' project setting in the API. !5930 (Duck)
- Reduce number of database queries on builds tab
+ - Wrap text in commit message containers
- Capitalize mentioned issue timeline notes (ClemMakesApps)
- Fix inconsistent checkbox alignment (ClemMakesApps)
- Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger)
- Adds response mime type to transaction metric action when it's not HTML
- Fix hover leading space bug in pipeline graph !5980
+ - Avoid conflict with admin labels when importing GitHub labels
- User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496
+ - Fix repository page ui issues
+ - Avoid protected branches checks when verifying access without branch name
+ - Add information about user and manual build start to runner as variables !6201 (Sergey Gnuskov)
- Fixed invisible scroll controls on build page on iPhone
-
-v 8.11.5 (unreleased)
- - Optimize branch lookups and force a repository reload for Repository#find_branch
+ - Fix error on raw build trace download for old builds stored in database !4822
+ - Refactor the triggers page and documentation !6217
+ - Show values of CI trigger variables only when clicked (Katarzyna Kobierska Ula Budziszewska)
+ - Use default clone protocol on "check out, review, and merge locally" help page URL
+ - Let the user choose a namespace and name on GitHub imports
+ - API for Ci Lint !5953 (Katarzyna Kobierska Urszula Budziszewska)
+ - Allow bulk update merge requests from merge requests index page
+ - Ensure validation messages are shown within the milestone form
+ - Add notification_settings API calls !5632 (mahcsig)
+ - Remove duplication between project builds and admin builds view !5680 (Katarzyna Kobierska Ula Budziszewska)
+ - Fix URLs with anchors in wiki !6300 (houqp)
+ - Deleting source project with existing fork link will close all related merge requests !6177 (Katarzyna Kobierska Ula Budziszeska)
+ - Return 204 instead of 404 for /ci/api/v1/builds/register.json if no builds are scheduled for a runner !6225
+ - Fix Gitlab::Popen.popen thread-safety issue
+ - Add specs to removing project (Katarzyna Kobierska Ula Budziszewska)
+ - Clean environment variables when running git hooks
+ - Fix Import/Export issues importing protected branches and some specific models
+ - Fix non-master branch readme display in tree view
+ - Add UX improvements for merge request version diffs
+
+v 8.11.8
+ - Respect the fork_project permission when forking projects
+ - Set a restrictive CORS policy on the API for credentialed requests
+ - API: disable rails session auth for non-GET/HEAD requests
+ - Escape HTML nodes in builds commands in CI linter
+
+v 8.11.7
+ - Avoid conflict with admin labels when importing GitHub labels. !6158
+ - Restores `fieldName` to allow only string values in `gl_dropdown.js`. !6234
+ - Allow the Rails cookie to be used for API authentication.
+
+v 8.11.6
+ - Fix unnecessary horizontal scroll area in pipeline visualizations. !6005
+ - Make merge conflict file size limit 200 KB, to match the docs. !6052
+ - Fix an error where we were unable to create a CommitStatus for running state. !6107
+ - Optimize discussion notes resolving and unresolving. !6141
+ - Fix GitLab import button. !6167
+ - Restore SSH Key title auto-population behavior. !6186
+ - Fix DB schema to match latest migration. !6256
+ - Exclude some pending or inactivated rows in Member scopes.
+
+v 8.11.5
+ - Optimize branch lookups and force a repository reload for Repository#find_branch. !6087
+ - Fix member expiration date picker after update. !6184
- Fix suggested colors options for new labels in the admin area. !6138
+ - Optimize discussion notes resolving and unresolving
+ - Fix GitLab import button
+ - Fix confidential issues being exposed as public using gitlab.com export
+ - Remove gitorious from import_sources. !6180
+ - Scope webhooks/services that will run for confidential issues
+ - Remove gitorious from import_sources
+ - Fix confidential issues being exposed as public using gitlab.com export
+ - Use oj gem for faster JSON processing
v 8.11.4
- Fix resolving conflicts on forks. !6082
@@ -90,6 +296,7 @@ v 8.11.4
- Fix issue boards leak private label names and descriptions
- Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner)
- Remove gitorious. !5866
+ - Allow compare merge request versions
v 8.11.3
- Allow system info page to handle case where info is unavailable
@@ -100,6 +307,7 @@ v 8.11.3
- Fix external issue tracker "Issues" link leading to 404s
- Don't try to show merge conflict resolution info if a merge conflict contains non-UTF-8 characters
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
+ - Issues filters reset button
v 8.11.2
- Show "Create Merge Request" widget for push events to fork projects on the source project. !5978
@@ -168,8 +376,6 @@ v 8.11.0
- Update `timeago` plugin to use multiple string/locale settings
- Remove unused images (ClemMakesApps)
- Get issue and merge request description templates from repositories
- - Add hover state to todos !5361 (winniehell)
- - Fix icon alignment of star and fork buttons !5451 (winniehell)
- Enforce 2FA restrictions on API authentication endpoints !5820
- Limit git rev-list output count to one in forced push check
- Show deployment status on merge requests with external URLs
@@ -266,6 +472,22 @@ v 8.11.0
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
+v 8.10.11
+ - Respect the fork_project permission when forking projects
+ - Set a restrictive CORS policy on the API for credentialed requests
+ - API: disable rails session auth for non-GET/HEAD requests
+ - Escape HTML nodes in builds commands in CI linter
+
+v 8.10.10
+ - Allow the Rails cookie to be used for API authentication.
+
+v 8.10.9
+ - Exclude some pending or inactivated rows in Member scopes
+
+v 8.10.8
+ - Fix information disclosure in issue boards.
+ - Fix privilege escalation in project import.
+
v 8.10.7
- Upgrade Hamlit to 2.6.1. !5873
- Upgrade Doorkeeper to 4.2.0. !5881
@@ -490,6 +712,19 @@ v 8.10.0
- Export and import avatar as part of project import/export
- Fix migration corrupting import data for old version upgrades
- Show tooltip on GitLab export link in new project page
+ - Fix import_data wrongly saved as a result of an invalid import_url !5206
+
+v 8.9.11
+ - Respect the fork_project permission when forking projects
+ - Set a restrictive CORS policy on the API for credentialed requests
+ - API: disable rails session auth for non-GET/HEAD requests
+ - Escape HTML nodes in builds commands in CI linter
+
+v 8.9.10
+ - Allow the Rails cookie to be used for API authentication.
+
+v 8.9.9
+ - Exclude some pending or inactivated rows in Member scopes
v 8.9.8
- Upgrade Doorkeeper to 4.2.0. !5881
@@ -507,12 +742,6 @@ v 8.9.6
- Keeps issue number when importing from Gitlab.com
- Add Pending tab for Builds (Katarzyna Kobierska, Urszula Budziszewska)
-v 8.9.7 (unreleased)
- - Fix import_data wrongly saved as a result of an invalid import_url
-
-v 8.9.6
- - Fix importing of events under notes for GitLab projects
-
v 8.9.5
- Add more debug info to import/export and memory killer. !5108
- Fixed avatar alignment in new MR view. !5095
@@ -1778,7 +2007,7 @@ v 8.1.3
- Use issue editor as cross reference comment author when issue is edited with a new mention
- Add Facebook authentication
-v 8.1.1
+v 8.1.2
- Fix cloning Wiki repositories via HTTP (Stan Hu)
- Add migration to remove satellites directory
- Fix specific runners visibility
@@ -1977,1692 +2206,5 @@ v 8.0.0
- Redirect from incorrectly cased group or project path to correct one (Francesco Levorato)
- Removed API calls from CE to CI
-v 7.14.3
- - No changes
-
-v 7.14.2
- - Upgrade gitlab_git to 7.2.15 to fix `git blame` errors with ISO-encoded files (Stan Hu)
- - Allow configuration of LDAP attributes GitLab will use for the new user account.
-
-v 7.14.1
- - Improve abuse reports management from admin area
- - Fix "Reload with full diff" URL button in compare branch view (Stan Hu)
- - Disabled DNS lookups for SSH in docker image (Rowan Wookey)
- - Only include base URL in OmniAuth full_host parameter (Stan Hu)
- - Fix Error 500 in API when accessing a group that has an avatar (Stan Hu)
- - Ability to enable SSL verification for Webhooks
-
-v 7.14.0
- - Fix bug where non-project members of the target project could set labels on new merge requests.
- - Update default robots.txt rules to disallow crawling of irrelevant pages (Ben Bodenmiller)
- - Fix redirection after sign in when using auto_sign_in_with_provider
- - Upgrade gitlab_git to 7.2.14 to ignore CRLFs in .gitmodules (Stan Hu)
- - Clear cache to prevent listing deleted branches after MR removes source branch (Stan Hu)
- - Provide more feedback what went wrong if HipChat service failed test (Stan Hu)
- - Fix bug where backslashes in inline diffs could be dropped (Stan Hu)
- - Disable turbolinks when linking to Bitbucket import status (Stan Hu)
- - Fix broken code import and display error messages if something went wrong with creating project (Stan Hu)
- - Fix corrupted binary files when using API files endpoint (Stan Hu)
- - Bump Haml to 4.0.7 to speed up textarea rendering (Stan Hu)
- - Show incompatible projects in Bitbucket import status (Stan Hu)
- - Fix coloring of diffs on MR Discussion-tab (Gert Goet)
- - Fix "Network" and "Graphs" pages for branches with encoded slashes (Stan Hu)
- - Fix errors deleting and creating branches with encoded slashes (Stan Hu)
- - Always add current user to autocomplete controller to support filter by "Me" (Stan Hu)
- - Fix multi-line syntax highlighting (Stan Hu)
- - Fix network graph when branch name has single quotes (Stan Hu)
- - Add "Confirm user" button in user admin page (Stan Hu)
- - Upgrade gitlab_git to version 7.2.6 to fix Error 500 when creating network graphs (Stan Hu)
- - Add support for Unicode filenames in relative links (Hiroyuki Sato)
- - Fix URL used for refreshing notes if relative_url is present (Bartłomiej Święcki)
- - Fix commit data retrieval when branch name has single quotes (Stan Hu)
- - Check that project was actually created rather than just validated in import:repos task (Stan Hu)
- - Fix full screen mode for snippet comments (Daniel Gerhardt)
- - Fix 404 error in files view after deleting the last file in a repository (Stan Hu)
- - Fix the "Reload with full diff" URL button (Stan Hu)
- - Fix label read access for unauthenticated users (Daniel Gerhardt)
- - Fix access to disabled features for unauthenticated users (Daniel Gerhardt)
- - Fix OAuth provider bug where GitLab would not go return to the redirect_uri after sign-in (Stan Hu)
- - Fix file upload dialog for comment editing (Daniel Gerhardt)
- - Set OmniAuth full_host parameter to ensure redirect URIs are correct (Stan Hu)
- - Return comments in created order in merge request API (Stan Hu)
- - Disable internal issue tracker controller if external tracker is used (Stan Hu)
- - Expire Rails cache entries after two weeks to prevent endless Redis growth
- - Add support for destroying project milestones (Stan Hu)
- - Allow custom backup archive permissions
- - Add project star and fork count, group avatar URL and user/group web URL attributes to API
- - Show who last edited a comment if it wasn't the original author
- - Send notification to all participants when MR is merged.
- - Add ability to manage user email addresses via the API.
- - Show buttons to add license, changelog and contribution guide if they're missing.
- - Tweak project page buttons.
- - Disabled autocapitalize and autocorrect on login field (Daryl Chan)
- - Mention group and project name in creation, update and deletion notices (Achilleas Pipinellis)
- - Update gravatar link on profile page to link to configured gravatar host (Ben Bodenmiller)
- - Remove redis-store TTL monkey patch
- - Add support for CI skipped status
- - Fetch code from forks to refs/merge-requests/:id/head when merge request created
- - Remove comments and email addresses when publicly exposing ssh keys (Zeger-Jan van de Weg)
- - Add "Check out branch" button to the MR page.
- - Improve MR merge widget text and UI consistency.
- - Improve text in MR "How To Merge" modal.
- - Cache all events
- - Order commits by date when comparing branches
- - Fix bug causing error when the target branch of a symbolic ref was deleted
- - Include branch/tag name in archive file and directory name
- - Add dropzone upload progress
- - Add a label for merged branches on branches page (Florent Baldino)
- - Detect .mkd and .mkdn files as markdown (Ben Boeckel)
- - Fix: User search feature in admin area does not respect filters
- - Set max-width for README, issue and merge request description for easier read on big screens
- - Update Flowdock integration to support new Flowdock API (Boyan Tabakov)
- - Remove author from files view (Sven Strickroth)
- - Fix infinite loop when SAML was incorrectly configured.
-
-v 7.13.5
- - Satellites reverted
-
-v 7.13.4
- - Allow users to send abuse reports
-
-v 7.13.3
- - Fix bug causing Bitbucket importer to crash when OAuth application had been removed.
- - Allow users to send abuse reports
- - Remove satellites
- - Link username to profile on Group Members page (Tom Webster)
-
-v 7.13.2
- - Fix randomly failed spec
- - Create project services on Project creation
- - Add admin_merge_request ability to Developer level and up
- - Fix Error 500 when browsing projects with no HEAD (Stan Hu)
- - Fix labels / assignee / milestone for the merge requests when issues are disabled
- - Show the first tab automatically on MergeRequests#new
- - Add rake task 'gitlab:update_commit_count' (Daniel Gerhardt)
- - Fix Gmail Actions
-
-v 7.13.1
- - Fix: Label modifications are not reflected in existing notes and in the issue list
- - Fix: Label not shown in the Issue list, although it's set through web interface
- - Fix: Group/project references are linked incorrectly
- - Improve documentation
- - Fix of migration: Check if session_expire_delay column exists before adding the column
- - Fix: ActionView::Template::Error
- - Fix: "Create Merge Request" isn't always shown in event for newly pushed branch
- - Fix bug causing "Remove source-branch" option not to work for merge requests from the same project.
- - Render Note field hints consistently for "new" and "edit" forms
-
-v 7.13.0
- - Remove repository graph log to fix slow cache updates after push event (Stan Hu)
- - Only enable HSTS header for HTTPS and port 443 (Stan Hu)
- - Fix user autocomplete for unauthenticated users accessing public projects (Stan Hu)
- - Fix redirection to home page URL for unauthorized users (Daniel Gerhardt)
- - Add branch switching support for graphs (Daniel Gerhardt)
- - Fix external issue tracker hook/test for HTTPS URLs (Daniel Gerhardt)
- - Remove link leading to a 404 error in Deploy Keys page (Stan Hu)
- - Add support for unlocking users in admin settings (Stan Hu)
- - Add Irker service configuration options (Stan Hu)
- - Fix order of issues imported from GitHub (Hiroyuki Sato)
- - Bump rugments to 1.0.0beta8 to fix C prototype function highlighting (Jonathon Reinhart)
- - Fix Merge Request webhook to properly fire "merge" action when accepted from the web UI
- - Add `two_factor_enabled` field to admin user API (Stan Hu)
- - Fix invalid timestamps in RSS feeds (Rowan Wookey)
- - Fix downloading of patches on public merge requests when user logged out (Stan Hu)
- - Fix Error 500 when relative submodule resolves to a namespace that has a different name from its path (Stan Hu)
- - Extract the longest-matching ref from a commit path when multiple matches occur (Stan Hu)
- - Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu)
- - Support commenting on diffs in side-by-side mode (Stan Hu)
- - Fix JavaScript error when clicking on the comment button on a diff line that has a comment already (Stan Hu)
- - Return 40x error codes if branch could not be deleted in UI (Stan Hu)
- - Remove project visibility icons from dashboard projects list
- - Rename "Design" profile settings page to "Preferences".
- - Allow users to customize their default Dashboard page.
- - Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8
- - Admin can edit and remove user identities
- - Convert CRLF newlines to LF when committing using the web editor.
- - API request /projects/:project_id/merge_requests?state=closed will return only closed merge requests without merged one. If you need ones that were merged - use state=merged.
- - Allow Administrators to filter the user list by those with or without Two-factor Authentication enabled.
- - Show a user's Two-factor Authentication status in the administration area.
- - Explicit error when commit not found in the CI
- - Improve performance for issue and merge request pages
- - Users with guest access level can not set assignee, labels or milestones for issue and merge request
- - Reporter role can manage issue tracker now: edit any issue, set assignee or milestone and manage labels
- - Better performance for pages with events list, issues list and commits list
- - Faster automerge check and merge itself when source and target branches are in same repository
- - Correctly show anonymous authorized applications under Profile > Applications.
- - Query Optimization in MySQL.
- - Allow users to be blocked and unblocked via the API
- - Use native Postgres database cleaning during backup restore
- - Redesign project page. Show README as default instead of activity. Move project activity to separate page
- - Make left menu more hierarchical and less contextual by adding back item at top
- - A fork can’t have a visibility level that is greater than the original project.
- - Faster code search in repository and wiki. Fixes search page timeout for big repositories
- - Allow administrators to disable 2FA for a specific user
- - Add error message for SSH key linebreaks
- - Store commits count in database (will populate with valid values only after first push)
- - Rebuild cache after push to repository in background job
- - Fix transferring of project to another group using the API.
-
-v 7.12.2
- - Correctly show anonymous authorized applications under Profile > Applications.
- - Faster automerge check and merge itself when source and target branches are in same repository
- - Audit log for user authentication
- - Allow custom label to be set for authentication providers.
-
-v 7.12.1
- - Fix error when deleting a user who has projects (Stan Hu)
- - Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu)
- - Add SAML to list of social_provider (Matt Firtion)
- - Fix merge requests API scope to keep compatibility in 7.12.x patch release (Dmitriy Zaporozhets)
- - Fix closed merge request scope at milestone page (Dmitriy Zaporozhets)
- - Revert merge request states renaming
- - Fix hooks for web based events with external issue references (Daniel Gerhardt)
- - Improve performance for issue and merge request pages
- - Compress database dumps to reduce backup size
-
-v 7.12.0
- - Fix Error 500 when one user attempts to access a personal, internal snippet (Stan Hu)
- - Disable changing of target branch in new merge request page when a branch has already been specified (Stan Hu)
- - Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu)
- - Update oauth button logos for Twitter and Google to recommended assets
- - Update browser gem to version 0.8.0 for IE11 support (Stan Hu)
- - Fix timeout when rendering file with thousands of lines.
- - Add "Remember me" checkbox to LDAP signin form.
- - Add session expiration delay configuration through UI application settings
- - Don't notify users mentioned in code blocks or blockquotes.
- - Omit link to generate labels if user does not have access to create them (Stan Hu)
- - Show warning when a comment will add 10 or more people to the discussion.
- - Disable changing of the source branch in merge request update API (Stan Hu)
- - Shorten merge request WIP text.
- - Add option to disallow users from registering any application to use GitLab as an OAuth provider
- - Support editing target branch of merge request (Stan Hu)
- - Refactor permission checks with issues and merge requests project settings (Stan Hu)
- - Fix Markdown preview not working in Edit Milestone page (Stan Hu)
- - Fix Zen Mode not closing with ESC key (Stan Hu)
- - Allow HipChat API version to be blank and default to v2 (Stan Hu)
- - Add file attachment support in Milestone description (Stan Hu)
- - Fix milestone "Browse Issues" button.
- - Set milestone on new issue when creating issue from index with milestone filter active.
- - Make namespace API available to all users (Stan Hu)
- - Add webhook support for note events (Stan Hu)
- - Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu)
- - Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu)
- - Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu)
- - Fix git blame syntax highlighting when different commits break up lines (Stan Hu)
- - Add "Resend confirmation e-mail" link in profile settings (Stan Hu)
- - Allow to configure location of the `.gitlab_shell_secret` file. (Jakub Jirutka)
- - Disabled expansion of top/bottom blobs for new file diffs
- - Update Asciidoctor gem to version 1.5.2. (Jakub Jirutka)
- - Fix resolving of relative links to repository files in AsciiDoc documents. (Jakub Jirutka)
- - Use the user list from the target project in a merge request (Stan Hu)
- - Default extention for wiki pages is now .md instead of .markdown (Jeroen van Baarsen)
- - Add validation to wiki page creation (only [a-zA-Z0-9/_-] are allowed) (Jeroen van Baarsen)
- - Fix new/empty milestones showing 100% completion value (Jonah Bishop)
- - Add a note when an Issue or Merge Request's title changes
- - Consistently refer to MRs as either Merged or Closed.
- - Add Merged tab to MR lists.
- - Prefix EmailsOnPush email subject with `[Git]`.
- - Group project contributions by both name and email.
- - Clarify navigation labels for Project Settings and Group Settings.
- - Move user avatar and logout button to sidebar
- - You can not remove user if he/she is an only owner of group
- - User should be able to leave group. If not - show him proper message
- - User has ability to leave project
- - Add SAML support as an omniauth provider
- - Allow to configure a URL to show after sign out
- - Add an option to automatically sign-in with an Omniauth provider
- - GitLab CI service sends .gitlab-ci.yml in each push call
- - When remove project - move repository and schedule it removal
- - Improve group removing logic
- - Trigger create-hooks on backup restore task
- - Add option to automatically link omniauth and LDAP identities
- - Allow special character in users bio. I.e.: I <3 GitLab
-
-v 7.11.4
- - Fix missing bullets when creating lists
- - Set rel="nofollow" on external links
-
-v 7.11.3
- - no changes
- - Fix upgrader script (Martins Polakovs)
-
-v 7.11.2
- - no changes
-
-v 7.11.1
- - no changes
-
-v 7.11.0
- - Fall back to Plaintext when Syntaxhighlighting doesn't work. Fixes some buggy lexers (Hannes Rosenögger)
- - Get editing comments to work in Chrome 43 again.
- - Fix broken view when viewing history of a file that includes a path that used to be another file (Stan Hu)
- - Don't show duplicate deploy keys
- - Fix commit time being displayed in the wrong timezone in some cases (Hannes Rosenögger)
- - Make the first branch pushed to an empty repository the default HEAD (Stan Hu)
- - Fix broken view when using a tag to display a tree that contains git submodules (Stan Hu)
- - Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu)
- - Add application setting to restrict user signups to e-mail domains (Stan Hu)
- - Don't allow a merge request to be merged when its title starts with "WIP".
- - Add a page title to every page.
- - Allow primary email to be set to an email that you've already added.
- - Fix clone URL field and X11 Primary selection (Dmitry Medvinsky)
- - Ignore invalid lines in .gitmodules
- - Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu)
- - Redirect to sign in page after signing out.
- - Fix "Hello @username." references not working by no longer allowing usernames to end in period.
- - Fix "Revspec not found" errors when viewing diffs in a forked project with submodules (Stan Hu)
- - Improve project page UI
- - Fix broken file browsing with relative submodule in personal projects (Stan Hu)
- - Add "Reply quoting selected text" shortcut key (`r`)
- - Fix bug causing `@whatever` inside an issue's first code block to be picked up as a user mention.
- - Fix bug causing `@whatever` inside an inline code snippet (backtick-style) to be picked up as a user mention.
- - When use change branches link at MR form - save source branch selection instead of target one
- - Improve handling of large diffs
- - Added GitLab Event header for project hooks
- - Add Two-factor authentication (2FA) for GitLab logins
- - Show Atom feed buttons everywhere where applicable.
- - Add project activity atom feed.
- - Don't crash when an MR from a fork has a cross-reference comment from the target project on one of its commits.
- - Explain how to get a new password reset token in welcome emails
- - Include commit comments in MR from a forked project.
- - Group milestones by title in the dashboard and all other issue views.
- - Query issues, merge requests and milestones with their IID through API (Julien Bianchi)
- - Add default project and snippet visibility settings to the admin web UI.
- - Show incompatible projects in Google Code import status (Stan Hu)
- - Fix bug where commit data would not appear in some subdirectories (Stan Hu)
- - Task lists are now usable in comments, and will show up in Markdown previews.
- - Fix bug where avatar filenames were not actually deleted from the database during removal (Stan Hu)
- - Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu)
- - Protect OmniAuth request phase against CSRF.
- - Don't send notifications to mentioned users that don't have access to the project in question.
- - Add search issues/MR by number
- - Change plots to bar graphs in commit statistics screen
- - Move snippets UI to fluid layout
- - Improve UI for sidebar. Increase separation between navigation and content
- - Improve new project command options (Ben Bodenmiller)
- - Add common method to force UTF-8 and use it to properly handle non-ascii OAuth user properties (Onur Küçük)
- - Prevent sending empty messages to HipChat (Chulki Lee)
- - Improve UI for mobile phones on dashboard and project pages
- - Add room notification and message color option for HipChat
- - Allow to use non-ASCII letters and dashes in project and namespace name. (Jakub Jirutka)
- - Add footnotes support to Markdown (Guillaume Delbergue)
- - Add current_sign_in_at to UserFull REST api.
- - Make Sidekiq MemoryKiller shutdown signal configurable
- - Add "Create Merge Request" buttons to commits and branches pages and push event.
- - Show user roles by comments.
- - Fix automatic blocking of auto-created users from Active Directory.
- - Call merge request webhook for each new commits (Arthur Gautier)
- - Use SIGKILL by default in Sidekiq::MemoryKiller
- - Fix mentioning of private groups.
- - Add style for <kbd> element in markdown
- - Spin spinner icon next to "Checking for CI status..." on MR page.
- - Fix reference links in dashboard activity and ATOM feeds.
- - Ensure that the first added admin performs repository imports
-
-v 7.10.4
- - Fix migrations broken in 7.10.2
- - Make tags for GitLab installations running on MySQL case sensitive
- - Get Gitorious importer to work again.
- - Fix adding new group members from admin area
- - Fix DB error when trying to tag a repository (Stan Hu)
- - Fix Error 500 when searching Wiki pages (Stan Hu)
- - Unescape branch names in compare commit (Stan Hu)
- - Order commit comments chronologically in API.
-
-v 7.10.2
- - Fix CI links on MR page
-
-v 7.10.0
- - Ignore submodules that are defined in .gitmodules but are checked in as directories.
- - Allow projects to be imported from Google Code.
- - Remove access control for uploaded images to fix broken images in emails (Hannes Rosenögger)
- - Allow users to be invited by email to join a group or project.
- - Don't crash when project repository doesn't exist.
- - Add config var to block auto-created LDAP users.
- - Don't use HTML ellipsis in EmailsOnPush subject truncated commit message.
- - Set EmailsOnPush reply-to address to committer email when enabled.
- - Fix broken file browsing with a submodule that contains a relative link (Stan Hu)
- - Fix persistent XSS vulnerability around profile website URLs.
- - Fix project import URL regex to prevent arbitary local repos from being imported.
- - Fix directory traversal vulnerability around uploads routes.
- - Fix directory traversal vulnerability around help pages.
- - Don't leak existence of project via search autocomplete.
- - Don't leak existence of group or project via search.
- - Fix bug where Wiki pages that included a '/' were no longer accessible (Stan Hu)
- - Fix bug where error messages from Dropzone would not be displayed on the issues page (Stan Hu)
- - Add a rake task to check repository integrity with `git fsck`
- - Add ability to configure Reply-To address in gitlab.yml (Stan Hu)
- - Move current user to the top of the list in assignee/author filters (Stan Hu)
- - Fix broken side-by-side diff view on merge request page (Stan Hu)
- - Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu)
- - Allow HTML tags in Markdown input
- - Fix code unfold not working on Compare commits page (Stan Hu)
- - Fix generating SSH key fingerprints with OpenSSH 6.8. (Sašo Stanovnik)
- - Fix "Import projects from" button to show the correct instructions (Stan Hu)
- - Fix dots in Wiki slugs causing errors (Stan Hu)
- - Make maximum attachment size configurable via Application Settings (Stan Hu)
- - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg)
- - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu)
- - Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu)
- - Reduce Rack Attack false positives causing 403 errors during HTTP authentication (Stan Hu)
- - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger)
- - Fix a link in the patch update guide
- - Add a service to support external wikis (Hannes Rosenögger)
- - Omit the "email patches" link and fix plain diff view for merge commits
- - List new commits for newly pushed branch in activity view.
- - Add sidetiq gem dependency to match EE
- - Add changelog, license and contribution guide links to project tab bar.
- - Improve diff UI
- - Fix alignment of navbar toggle button (Cody Mize)
- - Fix checkbox rendering for nested task lists
- - Identical look of selectboxes in UI
- - Upgrade the gitlab_git gem to version 7.1.3
- - Move "Import existing repository by URL" option to button.
- - Improve error message when save profile has error.
- - Passing the name of pushed ref to CI service (requires GitLab CI 7.9+)
- - Add location field to user profile
- - Fix print view for markdown files and wiki pages
- - Fix errors when deleting old backups
- - Improve GitLab performance when working with git repositories
- - Add tag message and last commit to tag hook (Kamil Trzciński)
- - Restrict permissions on backup files
- - Improve oauth accounts UI in profile page
- - Add ability to unlink connected accounts
- - Replace commits calendar with faster contribution calendar that includes issues and merge requests
- - Add inifinite scroll to user page activity
- - Don't include system notes in issue/MR comment count.
- - Don't mark merge request as updated when merge status relative to target branch changes.
- - Link note avatar to user.
- - Make Git-over-SSH errors more descriptive.
- - Fix EmailsOnPush.
- - Refactor issue filtering
- - AJAX selectbox for issue assignee and author filters
- - Fix issue with missing options in issue filtering dropdown if selected one
- - Prevent holding Control-Enter or Command-Enter from posting comment multiple times.
- - Prevent note form from being cleared when submitting failed.
- - Improve file icons rendering on tree (Sullivan Sénéchal)
- - API: Add pagination to project events
- - Get issue links in notification mail to work again.
- - Don't show commit comment button when user is not signed in.
- - Fix admin user projects lists.
- - Don't leak private group existence by redirecting from namespace controller to group controller.
- - Ability to skip some items from backup (database, respositories or uploads)
- - Archive repositories in background worker.
- - Import GitHub, Bitbucket or GitLab.com projects owned by authenticated user into current namespace.
- - Project labels are now available over the API under the "tag_list" field (Cristian Medina)
- - Fixed link paths for HTTP and SSH on the admin project view (Jeremy Maziarz)
- - Fix and improve help rendering (Sullivan Sénéchal)
- - Fix final line in EmailsOnPush email diff being rendered as error.
- - Prevent duplicate Buildkite service creation.
- - Fix git over ssh errors 'fatal: protocol error: bad line length character'
- - Automatically setup GitLab CI project for forks if origin project has GitLab CI enabled
- - Bust group page project list cache when namespace name or path changes.
- - Explicitly set image alt-attribute to prevent graphical glitches if gravatars could not be loaded
- - Allow user to choose a public email to show on public profile
- - Remove truncation from issue titles on milestone page (Jason Blanchard)
- - Fix stuck Merge Request merging events from old installations (Ben Bodenmiller)
- - Fix merge request comments on files with multiple commits
- - Fix Resource Owner Password Authentication Flow
- - Add icons to Add dropdown items.
- - Allow admin to create public deploy keys that are accessible to any project.
- - Warn when gitlab-shell version doesn't match requirement.
- - Skip email confirmation when set by admin or via LDAP.
- - Only allow users to reference groups, projects, issues, MRs, commits they have access to.
-
-v 7.9.4
- - Security: Fix project import URL regex to prevent arbitary local repos from being imported
- - Fixed issue where only 25 commits would load in file listings
- - Fix LDAP identities after config update
-
-v 7.9.3
- - Contains no changes
-
-v 7.9.2
- - Contains no changes
-
-v 7.9.1
- - Include missing events and fix save functionality in admin service template settings form (Stan Hu)
- - Fix "Import projects from" button to show the correct instructions (Stan Hu)
- - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu)
- - Fix for LDAP with commas in DN
- - Fix missing events and in admin Slack service template settings form (Stan Hu)
- - Don't show commit comment button when user is not signed in.
- - Downgrade gemnasium-gitlab-service gem
-
-v 7.9.0
- - Add HipChat integration documentation (Stan Hu)
- - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu)
- - Fix broken email images (Hannes Rosenögger)
- - Automatically config git if user forgot, where possible (Zeger-Jan van de Weg)
- - Fix mass SQL statements on initial push (Hannes Rosenögger)
- - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu)
- - Add comment notification events to HipChat and Slack services (Stan Hu)
- - Add issue and merge request events to HipChat and Slack services (Stan Hu)
- - Fix merge request URL passed to Webhooks. (Stan Hu)
- - Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu)
- - Fix code preview theme setting for comments, issues, merge requests, and snippets (Stan Hu)
- - Move labels/milestones tabs to sidebar
- - Upgrade Rails gem to version 4.1.9.
- - Improve error messages for file edit failures
- - Improve UI for commits, issues and merge request lists
- - Fix commit comments on first line of diff not rendering in Merge Request Discussion view.
- - Allow admins to override restricted project visibility settings.
- - Move restricted visibility settings from gitlab.yml into the web UI.
- - Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev)
- - Save web edit in new branch
- - Fix ordering of imported but unchanged projects (Marco Wessel)
- - Mobile UI improvements: make aside content expandable
- - Expose avatar_url in projects API
- - Fix checkbox alignment on the application settings page.
- - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger)
- - Fix mass-unassignment of issues (Robert Speicher)
- - Fix hidden diff comments in merge request discussion view
- - Allow user confirmation to be skipped for new users via API
- - Add a service to send updates to an Irker gateway (Romain Coltel)
- - Add brakeman (security scanner for Ruby on Rails)
- - Slack username and channel options
- - Add grouped milestones from all projects to dashboard.
- - Webhook sends pusher email as well as commiter
- - Add Bitbucket omniauth provider.
- - Add Bitbucket importer.
- - Support referencing issues to a project whose name starts with a digit
- - Condense commits already in target branch when updating merge request source branch.
- - Send notifications and leave system comments when bulk updating issues.
- - Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison)
- - Move groups page from profile to dashboard
- - Starred projects page at dashboard
- - Blocking user does not remove him/her from project/groups but show blocked label
- - Change subject of EmailsOnPush emails to include namespace, project and branch.
- - Change subject of EmailsOnPush emails to include first commit message when multiple were pushed.
- - Remove confusing footer from EmailsOnPush mail body.
- - Add list of changed files to EmailsOnPush emails.
- - Add option to send EmailsOnPush emails from committer email if domain matches.
- - Add option to disable code diffs in EmailOnPush emails.
- - Wrap commit message in EmailsOnPush email.
- - Send EmailsOnPush emails when deleting commits using force push.
- - Fix EmailsOnPush email comparison link to include first commit.
- - Fix highliht of selected lines in file
- - Reject access to group/project avatar if the user doesn't have access.
- - Add database migration to clean group duplicates with same path and name (Make sure you have a backup before update)
- - Add GitLab active users count to rake gitlab:check
- - Starred projects page at dashboard
- - Make email display name configurable
- - Improve json validation in hook data
- - Use Emoji One
- - Updated emoji help documentation to properly reference EmojiOne.
- - Fix missing GitHub organisation repositories on import page.
- - Added blue theme
- - Remove annoying notice messages when create/update merge request
- - Allow smb:// links in Markdown text.
- - Filter merge request by title or description at Merge Requests page
- - Block user if he/she was blocked in Active Directory
- - Fix import pages not working after first load.
- - Use custom LDAP label in LDAP signin form.
- - Execute hooks and services when branch or tag is created or deleted through web interface.
- - Block and unblock user if he/she was blocked/unblocked in Active Directory
- - Raise recommended number of unicorn workers from 2 to 3
- - Use same layout and interactivity for project members as group members.
- - Prevent gitlab-shell character encoding issues by receiving its changes as raw data.
- - Ability to unsubscribe/subscribe to issue or merge request
- - Delete deploy key when last connection to a project is destroyed.
- - Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther)
- - Backup of repositories with tar instead of git bundle (only now are git-annex files included in the backup)
- - Add canceled status for CI
- - Send EmailsOnPush email when branch or tag is created or deleted.
- - Faster merge request processing for large repository
- - Prevent doubling AJAX request with each commit visit via Turbolink
- - Prevent unnecessary doubling of js events on import pages and user calendar
-
-v 7.8.4
- - Fix issue_tracker_id substitution in custom issue trackers
- - Fix path and name duplication in namespaces
-
-v 7.8.3
- - Bump version of gitlab_git fixing annotated tags without message
-
-v 7.8.2
- - Fix service migration issue when upgrading from versions prior to 7.3
- - Fix setting of the default use project limit via admin UI
- - Fix showing of already imported projects for GitLab and Gitorious importers
- - Fix response of push to repository to return "Not found" if user doesn't have access
- - Fix check if user is allowed to view the file attachment
- - Fix import check for case sensetive namespaces
- - Increase timeout for Git-over-HTTP requests to 1 hour since large pulls/pushes can take a long time.
- - Properly handle autosave local storage exceptions.
- - Escape wildcards when searching LDAP by username.
-
-v 7.8.1
- - Fix run of custom post receive hooks
- - Fix migration that caused issues when upgrading to version 7.8 from versions prior to 7.3
- - Fix the warning for LDAP users about need to set password
- - Fix avatars which were not shown for non logged in users
- - Fix urls for the issues when relative url was enabled
-
-v 7.8.0
- - Fix access control and protection against XSS for note attachments and other uploads.
- - Replace highlight.js with rouge-fork rugments (Stefan Tatschner)
- - Make project search case insensitive (Hannes Rosenögger)
- - Include issue/mr participants in list of recipients for reassign/close/reopen emails
- - Expose description in groups API
- - Better UI for project services page
- - Cleaner UI for web editor
- - Add diff syntax highlighting in email-on-push service notifications (Hannes Rosenögger)
- - Add API endpoint to fetch all changes on a MergeRequest (Jeroen van Baarsen)
- - View note image attachments in new tab when clicked instead of downloading them
- - Improve sorting logic in UI and API. Explicitly define what sorting method is used by default
- - Fix overflow at sidebar when have several items
- - Add notes for label changes in issue and merge requests
- - Show tags in commit view (Hannes Rosenögger)
- - Only count a user's vote once on a merge request or issue (Michael Clarke)
- - Increase font size when browse source files and diffs
- - Service Templates now let you set default values for all services
- - Create new file in empty repository using GitLab UI
- - Ability to clone project using oauth2 token
- - Upgrade Sidekiq gem to version 3.3.0
- - Stop git zombie creation during force push check
- - Show success/error messages for test setting button in services
- - Added Rubocop for code style checks
- - Fix commits pagination
- - Async load a branch information at the commit page
- - Disable blacklist validation for project names
- - Allow configuring protection of the default branch upon first push (Marco Wessel)
- - Add gitlab.com importer
- - Add an ability to login with gitlab.com
- - Add a commit calendar to the user profile (Hannes Rosenögger)
- - Submit comment on command-enter
- - Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`.
- - Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" (Julien Bianchi and Hannes Rosenögger)
- - Fix long broadcast message cut-off on left sidebar (Visay Keo)
- - Add Project Avatars (Steven Thonus and Hannes Rosenögger)
- - Password reset token validity increased from 2 hours to 2 days since it is also send on account creation.
- - Edit group members via API
- - Enable raw image paste from clipboard, currently Chrome only (Marco Cyriacks)
- - Add action property to merge request hook (Julien Bianchi)
- - Remove duplicates from group milestone participants list.
- - Add a new API function that retrieves all issues assigned to a single milestone (Justin Whear and Hannes Rosenögger)
- - API: Access groups with their path (Julien Bianchi)
- - Added link to milestone and keeping resource context on smaller viewports for issues and merge requests (Jason Blanchard)
- - Allow notification email to be set separately from primary email.
- - API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger)
- - Don't have Markdown preview fail for long comments/wiki pages.
- - When test webhook - show error message instead of 500 error page if connection to hook url was reset
- - Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov)
- - Added persistent collapse button for left side nav bar (Jason Blanchard)
- - Prevent losing unsaved comments by automatically restoring them when comment page is loaded again.
- - Don't allow page to be scaled on mobile.
- - Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up.
- - Show assignees in merge request index page (Kelvin Mutuma)
- - Link head panel titles to relevant root page.
- - Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S).
- - Show users button to share their newly created public or internal projects on twitter
- - Add quick help links to the GitLab pricing and feature comparison pages.
- - Fix duplicate authorized applications in user profile and incorrect application client count in admin area.
- - Make sure Markdown previews always use the same styling as the eventual destination.
- - Remove deprecated Group#owner_id from API
- - Show projects user contributed to on user page. Show stars near project on user page.
- - Improve database performance for GitLab
- - Add Asana service (Jeremy Benoist)
- - Improve project webhooks with extra data
-
-v 7.7.2
- - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch
- - Fix issue when LDAP user can't login with existing GitLab account
-
-v 7.7.1
- - Improve mention autocomplete performance
- - Show setup instructions for GitHub import if disabled
- - Allow use http for OAuth applications
-
-v 7.7.0
- - Import from GitHub.com feature
- - Add Jetbrains Teamcity CI service (Jason Lippert)
- - Mention notification level
- - Markdown preview in wiki (Yuriy Glukhov)
- - Raise group avatar filesize limit to 200kb
- - OAuth applications feature
- - Show user SSH keys in admin area
- - Developer can push to protected branches option
- - Set project path instead of project name in create form
- - Block Git HTTP access after 10 failed authentication attempts
- - Updates to the messages returned by API (sponsored by O'Reilly Media)
- - New UI layout with side navigation
- - Add alert message in case of outdated browser (IE < 10)
- - Added API support for sorting projects
- - Update gitlab_git to version 7.0.0.rc14
- - Add API project search filter option for authorized projects
- - Fix File blame not respecting branch selection
- - Change some of application settings on fly in admin area UI
- - Redesign signin/signup pages
- - Close standard input in Gitlab::Popen.popen
- - Trigger GitLab CI when push tags
- - When accept merge request - do merge using sidaekiq job
- - Enable web signups by default
- - Fixes for diff comments: drag-n-drop images, selecting images
- - Fixes for edit comments: drag-n-drop images, preview mode, selecting images, save & update
- - Remove password strength indicator
-
-v 7.6.0
- - Fork repository to groups
- - New rugged version
- - Add CRON=1 backup setting for quiet backups
- - Fix failing wiki restore
- - Add optional Sidekiq MemoryKiller middleware (enabled via SIDEKIQ_MAX_RSS env variable)
- - Monokai highlighting style now more faithful to original design (Mark Riedesel)
- - Create project with repository in synchrony
- - Added ability to create empty repo or import existing one if project does not have repository
- - Reactivate highlight.js language autodetection
- - Mobile UI improvements
- - Change maximum avatar file size from 100KB to 200KB
- - Strict validation for snippet file names
- - Enable Markdown preview for issues, merge requests, milestones, and notes (Vinnie Okada)
- - In the docker directory is a container template based on the Omnibus packages.
- - Update Sidekiq to version 2.17.8
- - Add author filter to project issues and merge requests pages
- - Atom feed for user activity
- - Support multiple omniauth providers for the same user
- - Rendering cross reference in issue title and tooltip for merge request
- - Show username in comments
- - Possibility to create Milestones or Labels when Issues are disabled
- - Fix bug with showing gpg signature in tag
-
-v 7.5.3
- - Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2)
-
-v 7.5.2
- - Don't log Sidekiq arguments by default
- - Fix restore of wiki repositories from backups
-
-v 7.5.1
- - Add missing timestamps to 'members' table
-
-v 7.5.0
- - API: Add support for Hipchat (Kevin Houdebert)
- - Add time zone configuration in gitlab.yml (Sullivan Senechal)
- - Fix LDAP authentication for Git HTTP access
- - Run 'GC.start' after every EmailsOnPushWorker job
- - Fix LDAP config lookup for provider 'ldap'
- - Drop all sequences during Postgres database restore
- - Project title links to project homepage (Ben Bodenmiller)
- - Add Atlassian Bamboo CI service (Drew Blessing)
- - Mentioned @user will receive email even if he is not participating in issue or commit
- - Session API: Use case-insensitive authentication like in UI (Andrey Krivko)
- - Tie up loose ends with annotated tags: API & UI (Sean Edge)
- - Return valid json for deleting branch via API (sponsored by O'Reilly Media)
- - Expose username in project events API (sponsored by O'Reilly Media)
- - Adds comments to commits in the API
- - Performance improvements
- - Fix post-receive issue for projects with deleted forks
- - New gitlab-shell version with custom hooks support
- - Improve code
- - GitLab CI 5.2+ support (does not support older versions)
- - Fixed bug when you can not push commits starting with 000000 to protected branches
- - Added a password strength indicator
- - Change project name and path in one form
- - Display renamed files in diff views (Vinnie Okada)
- - Fix raw view for public snippets
- - Use secret token with GitLab internal API.
- - Add missing timestamps to 'members' table
-
-v 7.4.5
- - Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2)
-
-v 7.4.4
- - No changes
-
-v 7.4.3
- - Fix raw snippets view
- - Fix security issue for member api
- - Fix buildbox integration
-
-v 7.4.2
- - Fix internal snippet exposing for unauthenticated users
-
-v 7.4.1
- - Fix LDAP authentication for Git HTTP access
- - Fix LDAP config lookup for provider 'ldap'
- - Fix public snippets
- - Fix 500 error on projects with nested submodules
-
-v 7.4.0
- - Refactored membership logic
- - Improve error reporting on users API (Julien Bianchi)
- - Refactor test coverage tools usage. Use SIMPLECOV=true to generate it locally
- - Default branch is protected by default
- - Increase unicorn timeout to 60 seconds
- - Sort search autocomplete projects by stars count so most popular go first
- - Add README to tab on project show page
- - Do not delete tmp/repositories itself during clean-up, only its contents
- - Support for backup uploads to remote storage
- - Prevent notes polling when there are not notes
- - Internal ForkService: Prepare support for fork to a given namespace
- - API: Add support for forking a project via the API (Bernhard Kaindl)
- - API: filter project issues by milestone (Julien Bianchi)
- - Fail harder in the backup script
- - Changes to Slack service structure, only webhook url needed
- - Zen mode for wiki and milestones (Robert Schilling)
- - Move Emoji parsing to html-pipeline-gitlab (Robert Schilling)
- - Font Awesome 4.2 integration (Sullivan Senechal)
- - Add Pushover service integration (Sullivan Senechal)
- - Add select field type for services options (Sullivan Senechal)
- - Add cross-project references to the Markdown parser (Vinnie Okada)
- - Add task lists to issue and merge request descriptions (Vinnie Okada)
- - Snippets can be public, internal or private
- - Improve danger zone: ask project path to confirm data-loss action
- - Raise exception on forgery
- - Show build coverage in Merge Requests (requires GitLab CI v5.1)
- - New milestone and label links on issue edit form
- - Improved repository graphs
- - Improve event note display in dashboard and project activity views (Vinnie Okada)
- - Add users sorting to admin area
- - UI improvements
- - Fix ambiguous sha problem with mentioned commit
- - Fixed bug with apostrophe when at mentioning users
- - Add active directory ldap option
- - Developers can push to wiki repo. Protected branches does not affect wiki repo any more
- - Faster rev list
- - Fix branch removal
-
-v 7.3.2
- - Fix creating new file via web editor
- - Use gitlab-shell v2.0.1
-
-v 7.3.1
- - Fix ref parsing in Gitlab::GitAccess
- - Fix error 500 when viewing diff on a file with changed permissions
- - Fix adding comments to MR when source branch is master
- - Fix error 500 when searching description contains relative link
-
-v 7.3.0
- - Always set the 'origin' remote in satellite actions
- - Write authorized_keys in tmp/ during tests
- - Use sockets to connect to Redis
- - Add dormant New Relic gem (can be enabled via environment variables)
- - Expire Rack sessions after 1 week
- - Cleaner signin/signup pages
- - Improved comments UI
- - Better search with filtering, pagination etc
- - Added a checkbox to toggle line wrapping in diff (Yuriy Glukhov)
- - Prevent project stars duplication when fork project
- - Use the default Unicorn socket backlog value of 1024
- - Support Unix domain sockets for Redis
- - Store session Redis keys in 'session:gitlab:' namespace
- - Deprecate LDAP account takeover based on partial LDAP email / GitLab username match
- - Use /bin/sh instead of Bash in bin/web, bin/background_jobs (Pavel Novitskiy)
- - Keyboard shortcuts for productivity (Robert Schilling)
- - API: filter issues by state (Julien Bianchi)
- - API: filter issues by labels (Julien Bianchi)
- - Add system hook for ssh key changes
- - Add blob permalink link (Ciro Santilli)
- - Create annotated tags through UI and API (Sean Edge)
- - Snippets search (Charles Bushong)
- - Comment new push to existing MR
- - Add 'ci' to the blacklist of forbidden names
- - Improve text filtering on issues page
- - Comment & Close button
- - Process git push --all much faster
- - Don't allow edit of system notes
- - Project wiki search (Ralf Seidler)
- - Enabled Shibboleth authentication support (Matus Banas)
- - Zen mode (fullscreen) for issues/MR/notes (Robert Schilling)
- - Add ability to configure webhook timeout via gitlab.yml (Wes Gurney)
- - Sort project merge requests in asc or desc order for updated_at or created_at field (sponsored by O'Reilly Media)
- - Add Redis socket support to 'rake gitlab:shell:install'
-
-v 7.2.1
- - Delete orphaned labels during label migration (James Brooks)
- - Security: prevent XSS with stricter MIME types for raw repo files
-
-v 7.2.0
- - Explore page
- - Add project stars (Ciro Santilli)
- - Log Sidekiq arguments
- - Better labels: colors, ability to rename and remove
- - Improve the way merge request collects diffs
- - Improve compare page for large diffs
- - Expose the full commit message via API
- - Fix 500 error on repository rename
- - Fix bug when MR download patch return invalid diff
- - Test gitlab-shell integration
- - Repository import timeout increased from 2 to 4 minutes allowing larger repos to be imported
- - API for labels (Robert Schilling)
- - API: ability to set an import url when creating project for specific user
-
-v 7.1.1
- - Fix cpu usage issue in Firefox
- - Fix redirect loop when changing password by new user
- - Fix 500 error on new merge request page
-
-v 7.1.0
- - Remove observers
- - Improve MR discussions
- - Filter by description on Issues#index page
- - Fix bug with namespace select when create new project page
- - Show README link after description for non-master members
- - Add @all mention for comments
- - Dont show reply button if user is not signed in
- - Expose more information for issues with webhook
- - Add a mention of the merge request into the default merge request commit message
- - Improve code highlight, introduce support for more languages like Go, Clojure, Erlang etc
- - Fix concurrency issue in repository download
- - Dont allow repository name start with ?
- - Improve email threading (Pierre de La Morinerie)
- - Cleaner help page
- - Group milestones
- - Improved email notifications
- - Contributors API (sponsored by Mobbr)
- - Fix LDAP TLS authentication (Boris HUISGEN)
- - Show VERSION information on project sidebar
- - Improve branch removal logic when accept MR
- - Fix bug where comment form is spawned inside the Reply button
- - Remove Dir.chdir from Satellite#lock for thread-safety
- - Increased default git max_size value from 5MB to 20MB in gitlab.yml. Please update your configs!
- - Show error message in case of timeout in satellite when create MR
- - Show first 100 files for huge diff instead of hiding all
- - Change default admin email from admin@local.host to admin@example.com
-
-v 7.0.0
- - The CPU no longer overheats when you hold down the spacebar
- - Improve edit file UI
- - Add ability to upload group avatar when create
- - Protected branch cannot be removed
- - Developers can remove normal branches with UI
- - Remove branch via API (sponsored by O'Reilly Media)
- - Move protected branches page to Project settings area
- - Redirect to Files view when create new branch via UI
- - Drag and drop upload of image in every markdown-area (Earle Randolph Bunao and Neil Francis Calabroso)
- - Refactor the markdown relative links processing
- - Make it easier to implement other CI services for GitLab
- - Group masters can create projects in group
- - Deprecate ruby 1.9.3 support
- - Only masters can rewrite/remove git tags
- - Add X-Frame-Options SAMEORIGIN to Nginx config so Sidekiq admin is visible
- - UI improvements
- - Case-insensetive search for issues
- - Update to rails 4.1
- - Improve performance of application for projects and groups with a lot of members
- - Formally support Ruby 2.1
- - Include Nginx gitlab-ssl config
- - Add manual language detection for highlight.js
- - Added example.com/:username routing
- - Show notice if your profile is public
- - UI improvements for mobile devices
- - Improve diff rendering performance
- - Drag-n-drop for issues and merge requests between states at milestone page
- - Fix '0 commits' message for huge repositories on project home page
- - Prevent 500 error page when visit commit page from large repo
- - Add notice about huge push over http to unicorn config
- - File action in satellites uses default 30 seconds timeout instead of old 10 seconds one
- - Overall performance improvements
- - Skip init script check on omnibus-gitlab
- - Be more selective when killing stray Sidekiqs
- - Check LDAP user filter during sign-in
- - Remove wall feature (no data loss - you can take it from database)
- - Dont expose user emails via API unless you are admin
- - Detect issues closed by Merge Request description
- - Better email subject lines from email on push service (Alex Elman)
- - Enable identicon for gravatar be default
-
-v 6.9.2
- - Revert the commit that broke the LDAP user filter
-
-v 6.9.1
- - Fix scroll to highlighted line
- - Fix the pagination on load for commits page
-
-v 6.9.0
- - Store Rails cache data in the Redis `cache:gitlab` namespace
- - Adjust MySQL limits for existing installations
- - Add db index on project_id+iid column. This prevents duplicate on iid (During migration duplicates will be removed)
- - Markdown preview or diff during editing via web editor (Evgeniy Sokovikov)
- - Give the Rails cache its own Redis namespace
- - Add ability to set different ssh host, if different from http/https
- - Fix syntax highlighting for code comments blocks
- - Improve comments loading logic
- - Stop refreshing comments when the tab is hidden
- - Improve issue and merge request mobile UI (Drew Blessing)
- - Document how to convert a backup to PostgreSQL
- - Fix locale bug in backup manager
- - Fix can not automerge when MR description is too long
- - Fix wiki backup skip bug
- - Two Step MR creation process
- - Remove unwanted files from satellite working directory with git clean -fdx
- - Accept merge request via API (sponsored by O'Reilly Media)
- - Add more access checks during API calls
- - Block SSH access for 'disabled' Active Directory users
- - Labels for merge requests (Drew Blessing)
- - Threaded emails by setting a Message-ID (Philip Blatter)
-
-v 6.8.0
- - Ability to at mention users that are participating in issue and merge req. discussion
- - Enabled GZip Compression for assets in example Nginx, make sure that Nginx is compiled with --with-http_gzip_static_module flag (this is default in Ubuntu)
- - Make user search case-insensitive (Christopher Arnold)
- - Remove omniauth-ldap nickname bug workaround
- - Drop all tables before restoring a Postgres backup
- - Make the repository downloads path configurable
- - Create branches via API (sponsored by O'Reilly Media)
- - Changed permission of gitlab-satellites directory not to be world accessible
- - Protected branch does not allow force push
- - Fix popen bug in `rake gitlab:satellites:create`
- - Disable connection reaping for MySQL
- - Allow oauth signup without email for twitter and github
- - Fix faulty namespace names that caused 500 on user creation
- - Option to disable standard login
- - Clean old created archives from repository downloads directory
- - Fix download link for huge MR diffs
- - Expose event and mergerequest timestamps in API
- - Fix emails on push service when only one commit is pushed
-
-v 6.7.3
- - Fix the merge notification email not being sent (Pierre de La Morinerie)
- - Drop all tables before restoring a Postgres backup
- - Remove yanked modernizr gem
-
-v 6.7.2
- - Fix upgrader script
-
-v 6.7.1
- - Fix GitLab CI integration
-
-v 6.7.0
- - Increased the example Nginx client_max_body_size from 5MB to 20MB, consider updating it manually on existing installations
- - Add support for Gemnasium as a Project Service (Olivier Gonzalez)
- - Add edit file button to MergeRequest diff
- - Public groups (Jason Hollingsworth)
- - Cleaner headers in Notification Emails (Pierre de La Morinerie)
- - Blob and tree gfm links to anchors work
- - Piwik Integration (Sebastian Winkler)
- - Show contribution guide link for new issue form (Jeroen van Baarsen)
- - Fix CI status for merge requests from fork
- - Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard)
- - New page load indicator that includes a spinner that scrolls with the page
- - Converted all the help sections into markdown
- - LDAP user filters
- - Streamline the content of notification emails (Pierre de La Morinerie)
- - Fixes a bug with group member administration (Matt DeTullio)
- - Sort tag names using VersionSorter (Robert Speicher)
- - Add GFM autocompletion for MergeRequests (Robert Speicher)
- - Add webhook when a new tag is pushed (Jeroen van Baarsen)
- - Add button for toggling inline comments in diff view
- - Add retry feature for repository import
- - Reuse the GitLab LDAP connection within each request
- - Changed markdown new line behaviour to conform to markdown standards
- - Fix global search
- - Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5)
- - Create and Update MR calls now support the description parameter (Greg Messner)
- - Markdown relative links in the wiki link to wiki pages, markdown relative links in repositories link to files in the repository
- - Added Slack service integration (Federico Ravasio)
- - Better API responses for access_levels (sponsored by O'Reilly Media)
- - Requires at least 2 unicorn workers
- - Requires gitlab-shell v1.9+
- - Replaced gemoji(due to closed licencing problem) with Phantom Open Emoji library(combined SIL Open Font License, MIT License and the CC 3.0 License)
- - Fix `/:username.keys` response content type (Dmitry Medvinsky)
-
-v 6.6.5
- - Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard)
- - Hide mr close button for comment form if merge request was closed or inline comment
- - Adds ability to reopen closed merge request
-
-v 6.6.4
- - Add missing html escape for highlighted code blocks in comments, issues
-
-v 6.6.3
- - Fix 500 error when edit yourself from admin area
- - Hide private groups for public profiles
-
-v 6.6.2
- - Fix 500 error on branch/tag create or remove via UI
-
-v 6.6.1
- - Fix 500 error on files tab if submodules presents
-
-v 6.6.0
- - Retrieving user ssh keys publically(github style): http://__HOST__/__USERNAME__.keys
- - Permissions: Developer now can manage issue tracker (modify any issue)
- - Improve Code Compare page performance
- - Group avatar
- - Pygments.rb replaced with highlight.js
- - Improve Merge request diff store logic
- - Improve render performnace for MR show page
- - Fixed Assembla hardcoded project name
- - Jira integration documentation
- - Refactored app/services
- - Remove snippet expiration
- - Mobile UI improvements (Drew Blessing)
- - Fix block/remove UI for admin::users#show page
- - Show users' group membership on users' activity page (Robert Djurasaj)
- - User pages are visible without login if user is authorized to a public project
- - Markdown rendered headers have id derived from their name and link to their id
- - Improve application to work faster with large groups (100+ members)
- - Multiple emails per user
- - Show last commit for file when view file source
- - Restyle Issue#show page and MR#show page
- - Ability to filter by multiple labels for Issues page
- - Rails version to 4.0.3
- - Fixed attachment identifier displaying underneath note text (Jason Blanchard)
-
-v 6.5.1
- - Fix branch selectbox when create merge request from fork
-
-v 6.5.0
- - Dropdown menus on issue#show page for assignee and milestone (Jason Blanchard)
- - Add color custimization and previewing to broadcast messages
- - Fixed notes anchors
- - Load new comments in issues dynamically
- - Added sort options to Public page
- - New filters (assigned/authored/all) for Dashboard#issues/merge_requests (sponsored by Say Media)
- - Add project visibility icons to dashboard
- - Enable secure cookies if https used
- - Protect users/confirmation with rack_attack
- - Default HTTP headers to protect against MIME-sniffing, force https if enabled
- - Bootstrap 3 with responsive UI
- - New repository download formats: tar.bz2, zip, tar (Jason Hollingsworth)
- - Restyled accept widgets for MR
- - SCSS refactored
- - Use jquery timeago plugin
- - Fix 500 error for rdoc files
- - Ability to customize merge commit message (sponsored by Say Media)
- - Search autocomplete via ajax
- - Add website url to user profile
- - Files API supports base64 encoded content (sponsored by O'Reilly Media)
- - Added support for Go's repository retrieval (Bruno Albuquerque)
-
-v 6.4.3
- - Don't use unicorn worker killer if PhusionPassenger is defined
-
-v 6.4.2
- - Fixed wrong behaviour of script/upgrade.rb
-
-v 6.4.1
- - Fixed bug with repository rename
- - Fixed bug with project transfer
-
-v 6.4.0
- - Added sorting to project issues page (Jason Blanchard)
- - Assembla integration (Carlos Paramio)
- - Fixed another 500 error with submodules
- - UI: More compact issues page
- - Minimal password length increased to 8 symbols
- - Side-by-side diff view (Steven Thonus)
- - Internal projects (Jason Hollingsworth)
- - Allow removal of avatar (Drew Blessing)
- - Project webhooks now support issues and merge request events
- - Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth)
- - Expire event cache on avatar creation/removal (Drew Blessing)
- - Archiving old projects (Steven Thonus)
- - Rails 4
- - Add time ago tooltips to show actual date/time
- - UI: Fixed UI for admin system hooks
- - Ruby script for easier GitLab upgrade
- - Do not remove Merge requests if fork project was removed
- - Improve sign-in/signup UX
- - Add resend confirmation link to sign-in page
- - Set noreply@HOSTNAME for reply_to field in all emails
- - Show GitLab API version on Admin#dashboard
- - API Cross-origin resource sharing
- - Show READMe link at project home page
- - Show repo size for projects in Admin area
-
-v 6.3.0
- - API for adding gitlab-ci service
- - Init script now waits for pids to appear after (re)starting before reporting status (Rovanion Luckey)
- - Restyle project home page
- - Grammar fixes
- - Show branches list (which branches contains commit) on commit page (Andrew Kumanyaev)
- - Security improvements
- - Added support for GitLab CI 4.0
- - Fixed issue with 500 error when group did not exist
- - Ability to leave project
- - You can create file in repo using UI
- - You can remove file from repo using UI
- - API: dropped default_branch attribute from project during creation
- - Project default_branch is not stored in db any more. It takes from repo now.
- - Admin broadcast messages
- - UI improvements
- - Dont show last push widget if user removed this branch
- - Fix 500 error for repos with newline in file name
- - Extended html titles
- - API: create/update/delete repo files
- - Admin can transfer project to any namespace
- - API: projects/all for admin users
- - Fix recent branches order
-
-v 6.2.4
- - Security: Cast API private_token to string (CVE-2013-4580)
- - Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583)
- - Fix for Git SSH access for LDAP users
-
-v 6.2.3
- - Security: More protection against CVE-2013-4489
- - Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546)
- - Fix sidekiq rake tasks
-
-v 6.2.2
- - Security: Update gitlab_git (CVE-2013-4489)
-
-v 6.2.1
- - Security: Fix issue with generated passwords for new users
-
-v 6.2.0
- - Public project pages are now visible to everyone (files, issues, wik, etc.)
- THIS MEANS YOUR ISSUES AND WIKI FOR PUBLIC PROJECTS ARE PUBLICLY VISIBLE AFTER THE UPGRADE
- - Add group access to permissions page
- - Require current password to change one
- - Group owner or admin can remove other group owners
- - Remove group transfer since we have multiple owners
- - Respect authorization in Repository API
- - Improve UI for Project#files page
- - Add more security specs
- - Added search for projects by name to api (Izaak Alpert)
- - Make default user theme configurable (Izaak Alpert)
- - Update logic for validates_merge_request for tree of MR (Andrew Kumanyaev)
- - Rake tasks for webhooks management (Jonhnny Weslley)
- - Extended User API to expose admin and can_create_group for user creation/updating (Boyan Tabakov)
- - API: Remove group
- - API: Remove project
- - Avatar upload on profile page with a maximum of 100KB (Steven Thonus)
- - Store the sessions in Redis instead of the cookie store
- - Fixed relative links in markdown
- - User must confirm their email if signup enabled
- - User must confirm changed email
-
-v 6.1.0
- - Project specific IDs for issues, mr, milestones
- Above items will get a new id and for example all bookmarked issue urls will change.
- Old issue urls are redirected to the new one if the issue id is too high for an internal id.
- - Description field added to Merge Request
- - API: Sudo api calls (Izaak Alpert)
- - API: Group membership api (Izaak Alpert)
- - Improved commit diff
- - Improved large commit handling (Boyan Tabakov)
- - Rewrite: Init script now less prone to errors and keeps better track of the service (Rovanion Luckey)
- - Link issues, merge requests, and commits when they reference each other with GFM (Ash Wilson)
- - Close issues automatically when pushing commits with a special message
- - Improve user removal from admin area
- - Invalidate events cache when project was moved
- - Remove deprecated classes and rake tasks
- - Add event filter for group and project show pages
- - Add links to create branch/tag from project home page
- - Add public-project? checkbox to new-project view
- - Improved compare page. Added link to proceed into Merge Request
- - Send an email to a user when they are added to group
- - New landing page when you have 0 projects
-
-v 6.0.0
- - Feature: Replace teams with group membership
- We introduce group membership in 6.0 as a replacement for teams.
- The old combination of groups and teams was confusing for a lot of people.
- And when the members of a team where changed this wasn't reflected in the project permissions.
- In GitLab 6.0 you will be able to add members to a group with a permission level for each member.
- These group members will have access to the projects in that group.
- Any changes to group members will immediately be reflected in the project permissions.
- You can even have multiple owners for a group, greatly simplifying administration.
- - Feature: Ability to have multiple owners for group
- - Feature: Merge Requests between fork and project (Izaak Alpert)
- - Feature: Generate fingerprint for ssh keys
- - Feature: Ability to create and remove branches with UI
- - Feature: Ability to create and remove git tags with UI
- - Feature: Groups page in profile. You can leave group there
- - API: Allow login with LDAP credentials
- - Redesign: project settings navigation
- - Redesign: snippets area
- - Redesign: ssh keys page
- - Redesign: buttons, blocks and other ui elements
- - Add comment title to rss feed
- - You can use arrows to navigate at tree view
- - Add project filter on dashboard
- - Cache project graph
- - Drop support of root namespaces
- - Default theme is classic now
- - Cache result of methods like authorize_projects, project.team.members etc
- - Remove $.ready events
- - Fix onclick events being double binded
- - Add notification level to group membership
- - Move all project controllers/views under Projects:: module
- - Move all profile controllers/views under Profiles:: module
- - Apply user project limit only for personal projects
- - Unicorn is default web server again
- - Store satellites lock files inside satellites dir
- - Disabled threadsafety mode in rails
- - Fixed bug with loosing MR comments
- - Improved MR comments logic
- - Render readme file for projects in public area
-
-v 5.4.2
- - Security: Cast API private_token to string (CVE-2013-4580)
- - Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583)
-
-v 5.4.1
- - Security: Fixes for CVE-2013-4489
- - Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546)
-
-v 5.4.0
- - Ability to edit own comments
- - Documentation improvements
- - Improve dashboard projects page
- - Fixed nav for empty repos
- - GitLab Markdown help page
- - Misspelling fixes
- - Added support of unicorn and fog gems
- - Added client list to API doc
- - Fix PostgreSQL database restoration problem
- - Increase snippet content column size
- - allow project import via git:// url
- - Show participants on issues, including mentions
- - Notify mentioned users with email
-
-v 5.3.0
- - Refactored services
- - Campfire service added
- - HipChat service added
- - Fixed bug with LDAP + git over http
- - Fixed bug with google analytics code being ignored
- - Improve sign-in page if ldap enabled
- - Respect newlines in wall messages
- - Generate the Rails secret token on first run
- - Rename repo feature
- - Init.d: remove gitlab.socket on service start
- - Api: added teams api
- - Api: Prevent blob content being escaped
- - Api: Smart deploy key add behaviour
- - Api: projects/owned.json return user owned project
- - Fix bug with team assignation on project from #4109
- - Advanced snippets: public/private, project/personal (Andrew Kulakov)
- - Repository Graphs (Karlo Nicholas T. Soriano)
- - Fix dashboard lost if comment on commit
- - Update gitlab-grack. Fixes issue with --depth option
- - Fix project events duplicate on project page
- - Fix postgres error when displaying network graph.
- - Fix dashboard event filter when navigate via turbolinks
- - init.d: Ensure socket is removed before starting service
- - Admin area: Style teams:index, group:show pages
- - Own page for failed forking
- - Scrum view for milestone
-
-v 5.2.0
- - Turbolinks
- - Git over http with ldap credentials
- - Diff with better colors and some spacing on the corners
- - Default values for project features
- - Fixed huge_commit view
- - Restyle project clone panel
- - Move Gitlab::Git code to gitlab_git gem
- - Move update docs in repo
- - Requires gitlab-shell v1.4.0
- - Fixed submodules listing under file tab
- - Fork feature (Angus MacArthur)
- - git version check in gitlab:check
- - Shared deploy keys feature
- - Ability to generate default labels set for issues
- - Improve gfm autocomplete (Harold Luo)
- - Added support for Google Analytics
- - Code search feature (Javier Castro)
-
-v 5.1.0
- - You can login with email or username now
- - Corrected project transfer rollback when repository cannot be moved
- - Move both repo and wiki when project transfer requested
- - Admin area: project editing was removed from admin namespace
- - Access: admin user has now access to any project.
- - Notification settings
- - Gitlab::Git set of objects to abstract from grit library
- - Replace Unicorn web server with Puma
- - Backup/Restore refactored. Backup dump project wiki too now
- - Restyled Issues list. Show milestone version in issue row
- - Restyled Merge Request list
- - Backup now dump/restore uploads
- - Improved performance of dashboard (Andrew Kumanyaev)
- - File history now tracks renames (Akzhan Abdulin)
- - Drop wiki migration tools
- - Drop sqlite migration tools
- - project tagging
- - Paginate users in API
- - Restyled network graph (Hiroyuki Sato)
-
-v 5.0.1
- - Fixed issue with gitlab-grit being overridden by grit
-
-v 5.0.0
- - Replaced gitolite with gitlab-shell
- - Removed gitolite-related libraries
- - State machine added
- - Setup gitlab as git user
- - Internal API
- - Show team tab for empty projects
- - Import repository feature
- - Updated rails
- - Use lambda for scopes
- - Redesign admin area -> users
- - Redesign admin area -> user
- - Secure link to file attachments
- - Add validations for Group and Team names
- - Restyle team page for project
- - Update capybara, rspec-rails, poltergeist to recent versions
- - Wiki on git using Gollum
- - Added Solarized Dark theme for code review
- - Don't show user emails in autocomplete lists, profile pages
- - Added settings tab for group, team, project
- - Replace user popup with icons in header
- - Handle project moving with gitlab-shell
- - Added select2-rails for selectboxes with ajax data load
- - Fixed search field on projects page
- - Added teams to search autocomplete
- - Move groups and teams on dashboard sidebar to sub-tabs
- - API: improved return codes and docs. (Felix Gilcher, Sebastian Ziebell)
- - Redesign wall to be more like chat
- - Snippets, Wall features are disabled by default for new projects
-
-v 4.2.0
- - Teams
- - User show page. Via /u/username
- - Show help contents on pages for better navigation
- - Async gitolite calls
- - added satellites logs
- - can_create_group, can_create_team booleans for User
- - Process webhooks async
- - GFM: Fix images escaped inside links
- - Network graph improved
- - Switchable branches for network graph
- - API: Groups
- - Fixed project download
-
-v 4.1.0
- - Optional Sign-Up
- - Discussions
- - Satellites outside of tmp
- - Line numbers for blame
- - Project public mode
- - Public area with unauthorized access
- - Load dashboard events with ajax
- - remember dashboard filter in cookies
- - replace resque with sidekiq
- - fix routing issues
- - cleanup rake tasks
- - fix backup/restore
- - scss cleanup
- - show preview for note images
- - improved network-graph
- - get rid of app/roles/
- - added new classes Team, Repository
- - Reduce amount of gitolite calls
- - Ability to add user in all group projects
- - remove deprecated configs
- - replaced Korolev font with open font
- - restyled admin/dashboard page
- - restyled admin/projects page
-
-v 4.0.0
- - Remove project code and path from API. Use id instead
- - Return valid cloneable url to repo for webhook
- - Fixed backup issue
- - Reorganized settings
- - Fixed commits compare
- - Refactored scss
- - Improve status checks
- - Validates presence of User#name
- - Fixed postgres support
- - Removed sqlite support
- - Modified post-receive hook
- - Milestones can be closed now
- - Show comment events on dashboard
- - Quick add team members via group#people page
- - [API] expose created date for hooks and SSH keys
- - [API] list, create issue notes
- - [API] list, create snippet notes
- - [API] list, create wall notes
- - Remove project code - use path instead
- - added username field to user
- - rake task to fill usernames based on emails create namespaces for users
- - STI Group < Namespace
- - Project has namespace_id
- - Projects with namespaces also namespaced in gitolite and stored in subdir
- - Moving project to group will move it under group namespace
- - Ability to move project from namespaces to another
- - Fixes commit patches getting escaped (see #2036)
- - Support diff and patch generation for commits and merge request
- - MergeReqest doesn't generate a temporary file for the patch any more
- - Update the UI to allow downloading Patch or Diff
-
-v 3.1.0
- - Updated gems
- - Services: Gitlab CI integration
- - Events filter on dashboard
- - Own namespace for redis/resque
- - Optimized commit diff views
- - add alphabetical order for projects admin page
- - Improved web editor
- - Commit stats page
- - Documentation split and cleanup
- - Link to commit authors everywhere
- - Restyled milestones list
- - added Milestone to Merge Request
- - Restyled Top panel
- - Refactored Satellite Code
- - Added file line links
- - moved from capybara-webkit to poltergeist + phantomjs
-
-v 3.0.3
- - Fixed bug with issues list in Chrome
- - New Feature: Import team from another project
-
-v 3.0.2
- - Fixed gitlab:app:setup
- - Fixed application error on empty project in admin area
- - Restyled last push widget
-
-v 3.0.1
- - Fixed git over http
-
-v 3.0.0
- - Projects groups
- - Web Editor
- - Fixed bug with gitolite keys
- - UI improved
- - Increased performance of application
- - Show user avatar in last commit when browsing Files
- - Refactored Gitlab::Merge
- - Use Font Awesome for icons
- - Separate observing of Note and MergeRequests
- - Milestone "All Issues" filter
- - Fix issue close and reopen button text and styles
- - Fix forward/back while browsing Tree hierarchy
- - Show number of notes for commits and merge requests
- - Added support pg from box and update installation doc
- - Reject ssh keys that break gitolite
- - [API] list one project hook
- - [API] edit project hook
- - [API] list project snippets
- - [API] allow to authorize using private token in HTTP header
- - [API] add user creation
-
-v 2.9.1
- - Fixed resque custom config init
-
-v 2.9.0
- - fixed inline notes bugs
- - refactored rspecs
- - refactored gitolite backend
- - added factory_girl
- - restyled projects list on dashboard
- - ssh keys validation to prevent gitolite crash
- - send notifications if changed permission in project
- - scss refactoring. gitlab_bootstrap/ dir
- - fix git push http body bigger than 112k problem
- - list of labels page under issues tab
- - API for milestones, keys
- - restyled buttons
- - OAuth
- - Comment order changed
-
-v 2.8.1
- - ability to disable gravatars
- - improved MR diff logic
- - ssh key help page
-
-v 2.8.0
- - Gitlab Flavored Markdown
- - Bulk issues update
- - Issues API
- - Cucumber coverage increased
- - Post-receive files fixed
- - UI improved
- - Application cleanup
- - more cucumber
- - capybara-webkit + headless
-
-v 2.7.0
- - Issue Labels
- - Inline diff
- - Git HTTP
- - API
- - UI improved
- - System hooks
- - UI improved
- - Dashboard events endless scroll
- - Source performance increased
-
-v 2.6.0
- - UI polished
- - Improved network graph + keyboard nav
- - Handle huge commits
- - Last Push widget
- - Bugfix
- - Better performance
- - Email in resque
- - Increased test coverage
- - Ability to remove branch with MR accept
- - a lot of code refactored
-
-v 2.5.0
- - UI polished
- - Git blame for file
- - Bugfix
- - Email in resque
- - Better test coverage
-
-v 2.4.0
- - Admin area stats page
- - Ability to block user
- - Simplified dashboard area
- - Improved admin area
- - Bootstrap 2.0
- - Responsive layout
- - Big commits handling
- - Performance improved
- - Milestones
-
-v 2.3.1
- - Issues pagination
- - ssl fixes
- - Merge Request pagination
-
-v 2.3.0
- - Dashboard r1
- - Search r1
- - Project page
- - Close merge request on push
- - Persist MR diff after merge
- - mysql support
- - Documentation
-
-v 2.2.0
- - We’ve added support of LDAP auth
- - Improved permission logic (4 roles system)
- - Protected branches (now only masters can push to protected branches)
- - Usability improved
- - twitter bootstrap integrated
- - compare view between commits
- - wiki feature
- - now you can enable/disable issues, wiki, wall features per project
- - security fixes
- - improved code browsing (ajax branch switch etc)
- - improved per-line commenting
- - git submodules displayed
- - moved to rails 3.2
- - help section improved
-
-v 2.1.0
- - Project tab r1
- - List branches/tags
- - per line comments
- - mass user import
-
-v 2.0.0
- - gitolite as main git host system
- - merge requests
- - project/repo access
- - link to commit/issue feed
- - design tab
- - improved email notifications
- - restyled dashboard
- - bugfix
-
-v 1.2.2
- - common config file gitlab.yml
- - issues restyle
- - snippets restyle
- - clickable news feed header on dashboard
- - bugfix
-
-v 1.2.1
- - bugfix
-
-v 1.2.0
- - new design
- - user dashboard
- - network graph
- - markdown support for comments
- - encoding issues
- - wall like twitter timeline
-
-v 1.1.0
- - project dashboard
- - wall redesigned
- - feature: code snippets
- - fixed horizontal scroll on file preview
- - fixed app crash if commit message has invalid chars
- - bugfix & code cleaning
-
-v 1.0.2
- - fixed bug with empty project
- - added adv validation for project path & code
- - feature: issues can be sortable
- - bugfix
- - username displayed on top panel
-
-v 1.0.1
- - fixed: with invalid source code for commit
- - fixed: lose branch/tag selection when use tree navigation
- - when history clicked - display path
- - bug fix & code cleaning
-
-v 1.0.0
- - bug fix
- - projects preview mode
-
-v 0.9.6
- - css fix
- - new repo empty tree until restart server - fixed
-
-v 0.9.4
- - security improved
- - authorization improved
- - html escaping
- - bug fix
- - increased test coverage
- - design improvements
-
-v 0.9.1
- - increased test coverage
- - design improvements
- - new issue email notification
- - updated app name
- - issue redesigned
- - issue can be edit
-
-v 0.8.0
- - syntax highlight for main file types
- - redesign
- - stability
- - security fixes
- - increased test coverage
- - email notification
+v 7.14.3 through 0.8.0
+ - See changelogs/archive.md
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c77dcd96a7d..d5e15bfce14 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -91,19 +91,7 @@ This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs].
## Implement design & UI elements
-### Design reference
-
-The GitLab design reference can be found in the [gitlab-design] project.
-The designs are made using Antetype (`.atype` files). You can use the
-[free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design
-(the PNG is 1:1).
-
-The current designs can be found in the [`gitlab8.atype` file].
-
-### UI development kit
-
-Implemented UI elements can also be found at https://gitlab.com/help/ui. Please
-note that this page isn't comprehensive at this time.
+Please see the [UI Guide for building GitLab].
## Issue tracker
@@ -289,6 +277,8 @@ request is as follows:
1. For more complex migrations, write tests.
1. Merge requests **must** adhere to the [merge request performance
guidelines](doc/development/merge_request_performance_guidelines.md).
+1. For tests that use Capybara or PhantomJS, see this [article on how
+ to write reliable asynchronous tests](https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capybara).
The **official merge window** is in the beginning of the month from the 1st to
the 7th day of the month. This is the best time to submit an MR and get
@@ -489,7 +479,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
-[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design
-[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12
-[`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/
+[UI Guide for building GitLab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/ui_guide.md
[license-finder-doc]: doc/development/licensing.md
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 18091983f59..b72762837ea 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.4.0
+3.6.2
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index b4d6d12101f..100435be135 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.7.11
+0.8.2
diff --git a/Gemfile b/Gemfile
index 620338e5997..76ca6427feb 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,10 +6,8 @@ gem 'rails-deprecated_sanitizer', '~> 1.0.3'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
-# Specify a sprockets version due to increased performance
-# See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069
-gem 'sprockets', '~> 3.6.0'
-gem 'sprockets-es6'
+gem 'sprockets', '~> 3.7.0'
+gem 'sprockets-es6', '~> 0.9.2'
# Default values for AR models
gem 'default_value_for', '~> 3.0.0'
@@ -19,14 +17,14 @@ gem 'mysql2', '~> 0.3.16', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
# Authentication libraries
-gem 'devise', '~> 4.0'
+gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.1'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2'
-gem 'omniauth-facebook', '~> 3.0.0'
+gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.4.1'
@@ -53,7 +51,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.5'
+gem 'gitlab_git', '~> 10.6.7'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -122,8 +120,8 @@ gem 'diffy', '~> 3.0.3'
# Application server
group :unicorn do
- gem 'unicorn', '~> 4.9.0'
- gem 'unicorn-worker-killer', '~> 0.4.2'
+ gem 'unicorn', '~> 5.1.0'
+ gem 'unicorn-worker-killer', '~> 0.4.4'
end
# State machine
@@ -135,8 +133,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs
-gem 'sinatra', '~> 1.4.4', require: false
-gem 'sidekiq', '~> 4.0'
+gem 'sidekiq', '~> 4.2'
gem 'sidekiq-cron', '~> 0.4.0'
gem 'redis-namespace', '~> 1.5.2'
@@ -206,11 +203,14 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.3'
+# Faster JSON
+gem 'oj', '~> 2.17.4'
+
# Parse time & duration
gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6'
-gem 'sass-rails', '~> 5.0.0'
+gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
gem 'turbolinks', '~> 2.5.0'
@@ -295,11 +295,11 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
- gem 'rubocop', '~> 0.41.2', require: false
+ gem 'rubocop', '~> 0.42.0', require: false
gem 'rubocop-rspec', '~> 1.5.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
+ gem 'haml_lint', '~> 0.18.2', require: false
gem 'simplecov', '0.12.0', require: false
- gem 'flog', '~> 4.3.2', require: false
gem 'flay', '~> 2.6.1', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
@@ -316,10 +316,7 @@ group :test do
gem 'webmock', '~> 1.21.0'
gem 'test_after_commit', '~> 0.4.2'
gem 'sham_rack', '~> 1.3.6'
-end
-
-group :production do
- gem 'gitlab_meta', '7.0'
+ gem 'timecop', '~> 0.8.0'
end
gem 'newrelic_rpm', '~> 3.16'
@@ -330,7 +327,7 @@ gem 'mail_room', '~> 0.8'
gem 'email_reply_parser', '~> 0.5.8'
-gem 'ruby-prof', '~> 0.15.9'
+gem 'ruby-prof', '~> 0.16.2'
## CI
gem 'activerecord-session_store', '~> 1.0.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 28ede86b3ba..f15715a20ff 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -161,7 +161,7 @@ GEM
activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
- devise (4.1.1)
+ devise (4.2.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0, < 5.1)
@@ -189,7 +189,7 @@ GEM
erubis (2.7.0)
escape_utils (1.1.1)
eventmachine (1.0.8)
- excon (0.49.0)
+ excon (0.52.0)
execjs (2.6.0)
expression_parser (0.9.0)
factory_girl (4.5.0)
@@ -209,14 +209,11 @@ GEM
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-aws (0.9.2)
- fog-core (~> 1.27)
+ fog-aws (0.11.0)
+ fog-core (~> 1.38)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
@@ -225,7 +222,7 @@ GEM
fog-core (~> 1.27)
fog-json (~> 1.0)
fog-xml (~> 0.1)
- fog-core (1.40.0)
+ fog-core (1.42.0)
builder
excon (~> 0.49)
formatador (~> 0.2)
@@ -279,12 +276,11 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab_git (10.5.0)
+ gitlab_git (10.6.7)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
rugged (~> 0.24.0)
- gitlab_meta (7.0)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
@@ -322,11 +318,18 @@ GEM
grape-entity (0.4.8)
activesupport
multi_json (>= 1.3.2)
+ haml (4.0.7)
+ tilt
+ haml_lint (0.18.2)
+ haml (~> 4.0)
+ rake (>= 10, < 12)
+ rubocop (>= 0.36.0)
+ sysexits (~> 1.1)
hamlit (2.6.1)
temple (~> 0.7.6)
thor
tilt
- hashie (3.4.3)
+ hashie (3.4.4)
health_check (2.1.0)
rails (>= 4.0)
hipchat (1.5.2)
@@ -394,7 +397,7 @@ GEM
mime-types (>= 1.16, < 4)
mail_room (0.8.0)
method_source (0.8.2)
- mime-types (2.99.2)
+ mime-types (2.99.3)
mimemagic (0.3.0)
mini_portile2 (2.1.0)
minitest (5.7.0)
@@ -420,6 +423,7 @@ GEM
rack (>= 1.2, < 3)
octokit (4.3.0)
sawyer (~> 0.7.0, >= 0.5.3)
+ oj (2.17.4)
omniauth (1.3.1)
hashie (>= 1.2, < 4)
rack (>= 1.0, < 3)
@@ -437,7 +441,7 @@ GEM
addressable (~> 2.3)
nokogiri (~> 1.6.6)
omniauth (~> 1.2)
- omniauth-facebook (3.0.0)
+ omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2)
omniauth-github (1.1.2)
omniauth (~> 1.0)
@@ -546,7 +550,7 @@ GEM
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.1.0)
- raindrops (0.15.0)
+ raindrops (0.17.0)
rake (10.5.0)
rb-fsevent (0.9.6)
rb-inotify (0.9.5)
@@ -580,11 +584,11 @@ GEM
request_store (1.3.1)
rerun (0.11.0)
listen (~> 3.0)
- responders (2.1.1)
+ responders (2.3.0)
railties (>= 4.2.0, < 5.1)
rinku (2.0.0)
rotp (2.1.2)
- rouge (2.0.5)
+ rouge (2.0.6)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -612,7 +616,7 @@ GEM
rspec-retry (0.4.5)
rspec-core
rspec-support (3.5.0)
- rubocop (0.41.2)
+ rubocop (0.42.0)
parser (>= 2.3.1.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
@@ -622,7 +626,7 @@ GEM
rubocop (>= 0.40.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
- ruby-prof (0.15.9)
+ ruby-prof (0.16.2)
ruby-progressbar (1.8.1)
ruby-saml (1.3.0)
nokogiri (>= 1.5.10)
@@ -637,7 +641,7 @@ GEM
sanitize (2.1.0)
nokogiri (>= 1.4.4)
sass (3.4.22)
- sass-rails (5.0.5)
+ sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
@@ -665,11 +669,11 @@ GEM
rack
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
- sidekiq (4.1.4)
+ sidekiq (4.2.1)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
+ rack-protection (~> 1.5)
redis (~> 3.2, >= 3.2.1)
- sinatra (>= 1.4.7)
sidekiq-cron (0.4.0)
redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24)
@@ -679,10 +683,6 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
- sinatra (1.4.7)
- rack (~> 1.5)
- rack-protection (~> 1.4)
- tilt (>= 1.3, < 3)
slack-notifier (1.2.1)
slop (3.6.0)
spinach (0.8.10)
@@ -702,10 +702,10 @@ GEM
spring (>= 0.9.1)
spring-commands-teaspoon (0.0.2)
spring (>= 0.9.1)
- sprockets (3.6.3)
+ sprockets (3.7.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
- sprockets-es6 (0.9.0)
+ sprockets-es6 (0.9.2)
babel-source (>= 5.8.11)
babel-transpiler
sprockets (>= 3.0.0)
@@ -723,6 +723,7 @@ GEM
stringex (2.5.2)
sys-filesystem (1.1.6)
ffi
+ sysexits (1.2.0)
systemu (2.6.5)
task_list (1.0.2)
html-pipeline
@@ -754,10 +755,9 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
- unicode-display_width (1.1.0)
- unicorn (4.9.0)
+ unicode-display_width (1.1.1)
+ unicorn (5.1.0)
kgio (~> 2.6)
- rack
raindrops (~> 0.7)
unicorn-worker-killer (0.4.4)
get_process_mem (~> 0)
@@ -832,7 +832,7 @@ DEPENDENCIES
d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
default_value_for (~> 3.0.0)
- devise (~> 4.0)
+ devise (~> 4.2)
devise-two-factor (~> 3.0.0)
diffy (~> 3.0.3)
doorkeeper (~> 4.2.0)
@@ -842,7 +842,6 @@ DEPENDENCIES
factory_girl_rails (~> 4.6.0)
ffaker (~> 2.0.0)
flay (~> 2.6.1)
- flog (~> 4.3.2)
fog-aws (~> 0.9)
fog-azure (~> 0.0)
fog-core (~> 1.40)
@@ -858,14 +857,14 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab_git (~> 10.5)
- gitlab_meta (= 7.0)
+ gitlab_git (~> 10.6.7)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.1.0)
grape (~> 0.15.0)
grape-entity (~> 0.4.2)
+ haml_lint (~> 0.18.2)
hamlit (~> 2.6.1)
health_check (~> 2.1.0)
hipchat (~> 1.5.0)
@@ -895,12 +894,13 @@ DEPENDENCIES
nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.2.0)
octokit (~> 4.3.0)
+ oj (~> 2.17.4)
omniauth (~> 1.3.1)
omniauth-auth0 (~> 1.4.1)
omniauth-azure-oauth2 (~> 0.0.6)
omniauth-bitbucket (~> 0.0.2)
omniauth-cas3 (~> 1.1.2)
- omniauth-facebook (~> 3.0.0)
+ omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.4.1)
@@ -935,12 +935,12 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
- rubocop (~> 0.41.2)
+ rubocop (~> 0.42.0)
rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1)
- ruby-prof (~> 0.15.9)
+ ruby-prof (~> 0.16.2)
sanitize (~> 2.0)
- sass-rails (~> 5.0.0)
+ sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0)
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
@@ -949,10 +949,9 @@ DEPENDENCIES
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
- sidekiq (~> 4.0)
+ sidekiq (~> 4.2)
sidekiq-cron (~> 0.4.0)
simplecov (= 0.12.0)
- sinatra (~> 1.4.4)
slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
@@ -960,8 +959,8 @@ DEPENDENCIES
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.1.0)
spring-commands-teaspoon (~> 0.0.2)
- sprockets (~> 3.6.0)
- sprockets-es6
+ sprockets (~> 3.7.0)
+ sprockets-es6 (~> 0.9.2)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
task_list (~> 1.0.2)
@@ -969,13 +968,14 @@ DEPENDENCIES
teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2)
thin (~> 1.7.0)
+ timecop (~> 0.8.0)
turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0)
unf (~> 0.1.4)
- unicorn (~> 4.9.0)
- unicorn-worker-killer (~> 0.4.2)
+ unicorn (~> 5.1.0)
+ unicorn-worker-killer (~> 0.4.4)
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.2)
@@ -984,4 +984,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.12.5
+ 1.13.1
diff --git a/README.md b/README.md
index 3df8bfa04c7..8236f986b56 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,11 @@
[![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
+[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
## Canonical source
-The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/) and there are mirrors to make [contributing](CONTRIBUTING.md) as easy as possible.
+The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
## Open source software to collaborate on code
diff --git a/VERSION b/VERSION
index 8a5bbc87ef9..dff4cd02d5f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.12.0-pre
+8.13.0-pre
diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png
deleted file mode 100644
index 5b55e12571c..00000000000
--- a/app/assets/images/icon-link.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/icon_anchor.svg b/app/assets/images/icon_anchor.svg
new file mode 100644
index 00000000000..7e242586bad
--- /dev/null
+++ b/app/assets/images/icon_anchor.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#333" fill-rule="evenodd" d="M9.683 6.676l-.047-.048C8.27 5.26 6.07 5.243 4.726 6.588l-2.29 2.29c-1.344 1.344-1.328 3.544.04 4.91 1.366 1.368 3.564 1.385 4.908.04l1.753-1.752c-.695.074-1.457-.078-2.176-.444L5.934 12.66c-.634.634-1.67.625-2.312-.017-.642-.643-.65-1.677-.017-2.312L6.035 7.9c.634-.634 1.67-.625 2.312.017.024.024.048.05.07.075l.003-.002c.36.36.943.366 1.3.01.355-.356.35-.938-.01-1.3l-.027-.024zM6.58 9.586l.048.05c1.367 1.366 3.565 1.384 4.91.04l2.29-2.292c1.344-1.343 1.328-3.542-.04-4.91-1.366-1.366-3.564-1.384-4.908-.04L7.127 4.187c.695-.074 1.457.078 2.176.444l1.028-1.027c.635-.634 1.67-.624 2.313.017.643.644.652 1.678.018 2.312l-2.43 2.432c-.635.634-1.67.624-2.313-.018-.024-.024-.048-.05-.07-.075l-.003.004c-.36-.362-.943-.367-1.3-.01-.355.355-.35.937.01 1.3.01.007.018.015.027.023z"/></svg> \ No newline at end of file
diff --git a/app/assets/javascripts/LabelManager.js b/app/assets/javascripts/LabelManager.js
index 151455ce4a3..d4a4c7abaa1 100644
--- a/app/assets/javascripts/LabelManager.js
+++ b/app/assets/javascripts/LabelManager.js
@@ -3,6 +3,7 @@
LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time';
function LabelManager(opts) {
+ // Defaults
var ref, ref1, ref2;
if (opts == null) {
opts = {};
@@ -28,6 +29,7 @@
$btn = $(e.currentTarget);
$label = $("#" + ($btn.data('domId')));
action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
+ // Make sure tooltip will hide
$tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby')));
$tooltip.tooltip('destroy');
return _this.toggleLabelPriority($label, action);
@@ -42,6 +44,7 @@
url = $label.find('.js-toggle-priority').data('url');
$target = this.prioritizedLabels;
$from = this.otherLabels;
+ // Optimistic update
if (action === 'remove') {
$target = this.otherLabels;
$from = this.prioritizedLabels;
@@ -53,6 +56,7 @@
$target.find('.empty-message').addClass('hidden');
}
$label.detach().appendTo($target);
+ // Return if we are not persisting state
if (!persistState) {
return;
}
@@ -61,6 +65,7 @@
url: url,
type: 'DELETE'
});
+ // Restore empty message
if (!$from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 84b292e59c6..1cd2302111e 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -16,20 +16,18 @@
.replace(':id', group_id);
return $.ajax({
url: url,
- data: {
- private_token: gon.api_token
- },
dataType: "json"
}).done(function(group) {
return callback(group);
});
},
+ // Return groups list. Filtered by query
+ // Only active groups retrieved
groups: function(query, skip_ldap, callback) {
var url = Api.buildUrl(Api.groupsPath);
return $.ajax({
url: url,
data: {
- private_token: gon.api_token,
search: query,
per_page: 20
},
@@ -38,12 +36,12 @@
return callback(groups);
});
},
+ // Return namespaces list. Filtered by query
namespaces: function(query, callback) {
var url = Api.buildUrl(Api.namespacesPath);
return $.ajax({
url: url,
data: {
- private_token: gon.api_token,
search: query,
per_page: 20
},
@@ -52,12 +50,12 @@
return callback(namespaces);
});
},
+ // Return projects list. Filtered by query
projects: function(query, order, callback) {
var url = Api.buildUrl(Api.projectsPath);
return $.ajax({
url: url,
data: {
- private_token: gon.api_token,
search: query,
order_by: order,
per_page: 20
@@ -70,7 +68,6 @@
newLabel: function(project_id, data, callback) {
var url = Api.buildUrl(Api.labelsPath)
.replace(':id', project_id);
- data.private_token = gon.api_token;
return $.ajax({
url: url,
type: "POST",
@@ -82,13 +79,13 @@
return callback(message.responseJSON);
});
},
+ // Return group projects list. Filtered by query
groupProjects: function(group_id, query, callback) {
var url = Api.buildUrl(Api.groupProjectsPath)
.replace(':id', group_id);
return $.ajax({
url: url,
data: {
- private_token: gon.api_token,
search: query,
per_page: 20
},
@@ -97,6 +94,7 @@
return callback(projects);
});
},
+ // Return text for a specific license
licenseText: function(key, data, callback) {
var url = Api.buildUrl(Api.licensePath)
.replace(':key', key);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 43a679501a7..8a61669822c 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,3 +1,9 @@
+// This is a manifest file that'll be compiled into including all the files listed below.
+// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+// be included in the compiled file accessible from http://example.com/assets/application.js
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// the compiled file.
+//
/*= require jquery2 */
/*= require jquery-ui/autocomplete */
/*= require jquery-ui/datepicker */
@@ -76,6 +82,7 @@
}
};
+ // Disable button if text field is empty
window.disableButtonIfEmptyField = function(field_selector, button_selector) {
var closest_submit, field;
field = $(field_selector);
@@ -92,6 +99,7 @@
});
};
+ // Disable button if any input field with given selector is empty
window.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) {
var closest_submit, updateButtons;
closest_submit = form.find(button_selector);
@@ -128,6 +136,8 @@
window.addEventListener("hashchange", shiftWindow);
window.onload = function() {
+ // Scroll the window to avoid the topnav bar
+ // https://github.com/twitter/bootstrap/issues/1768
if (location.hash) {
return setTimeout(shiftWindow, 100);
}
@@ -149,6 +159,8 @@
return $(this).select().one('mouseup', function(e) {
return e.preventDefault();
});
+ // Click a .js-select-on-focus field, select the contents
+ // Prevent a mouseup event from deselecting the input
});
$('.remove-row').bind('ajax:success', function() {
$(this).tooltip('destroy')
@@ -163,6 +175,7 @@
});
$('select.select2').select2({
width: 'resolve',
+ // Initialize select2 selects
dropdownAutoWidth: true
});
$('.js-select2').bind('select2-close', function() {
@@ -170,25 +183,28 @@
$('.select2-container-active').removeClass('select2-container-active');
return $(':focus').blur();
}), 1);
+ // Close select2 on escape
});
+ // Initialize tooltips
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
placement: function(_, el) {
- var $el;
- $el = $(el);
- return $el.data('placement') || 'bottom';
+ return $(el).data('placement') || 'bottom';
}
});
$('.trigger-submit').on('change', function() {
return $(this).parents('form').submit();
+ // Form submitter
});
gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
+ // Flash
if ((flash = $(".flash-container")).length > 0) {
flash.click(function() {
return $(this).fadeOut();
});
flash.show();
}
+ // Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function(e) {
var buttons;
buttons = $('[type="submit"]', this);
@@ -209,6 +225,7 @@
}
});
$('.account-box').hover(function() {
+ // Show/Hide the profile menu when hovering the account box
return $(this).toggleClass('hover');
});
$document.on('click', '.diff-content .js-show-suppressed-diff', function() {
@@ -216,6 +233,7 @@
$container = $(this).parent();
$container.next('table').show();
return $container.remove();
+ // Commit show suppressed diff
});
$('.navbar-toggle').on('click', function() {
$('.header-content .title').toggle();
@@ -223,15 +241,17 @@
$('.header-content .navbar-collapse').toggle();
return $('.navbar-toggle').toggleClass('active');
});
+ // Show/hide comments on diff
$body.on("click", ".js-toggle-diff-comments", function(e) {
var $this = $(this);
$this.toggleClass('active');
var notesHolders = $this.closest('.diff-file').find('.notes_holder');
if ($this.hasClass('active')) {
- notesHolders.show();
+ notesHolders.show().find('.hide').show();
} else {
notesHolders.hide();
}
+ $this.trigger('blur');
return e.preventDefault();
});
$document.off("click", '.js-confirm-danger');
@@ -286,42 +306,9 @@
gl.awardsHandler = new AwardsHandler();
checkInitialSidebarSize();
new Aside();
- if ($window.width() < 1024 && $.cookie('pin_nav') === 'true') {
- $.cookie('pin_nav', 'false', {
- path: gon.relative_url_root || '/',
- expires: 365 * 10
- });
- $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned');
- $('.navbar-fixed-top').removeClass('header-pinned-nav');
- }
- $document.off('click', '.js-nav-pin').on('click', '.js-nav-pin', function(e) {
- var $page, $pinBtn, $tooltip, $topNav, doPinNav, tooltipText;
- e.preventDefault();
- $pinBtn = $(e.currentTarget);
- $page = $('.page-with-sidebar');
- $topNav = $('.navbar-fixed-top');
- $tooltip = $("#" + ($pinBtn.attr('aria-describedby')));
- doPinNav = !$page.is('.page-sidebar-pinned');
- tooltipText = 'Pin navigation';
- $(this).toggleClass('is-active');
- if (doPinNav) {
- $page.addClass('page-sidebar-pinned');
- $topNav.addClass('header-pinned-nav');
- } else {
- $tooltip.remove();
- $page.removeClass('page-sidebar-pinned').toggleClass('page-sidebar-collapsed page-sidebar-expanded');
- $topNav.removeClass('header-pinned-nav').toggleClass('header-collapsed header-expanded');
- }
- $.cookie('pin_nav', doPinNav, {
- path: gon.relative_url_root || '/',
- expires: 365 * 10
- });
- if ($.cookie('pin_nav') === 'true' || doPinNav) {
- tooltipText = 'Unpin navigation';
- }
- $tooltip.find('.tooltip-inner').text(tooltipText);
- return $pinBtn.attr('title', tooltipText).tooltip('fixTitle');
- });
+
+ // bind sidebar events
+ new gl.Sidebar();
// Custom time ago
gl.utils.shortTimeAgo($('.js-short-timeago'));
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 7116512d6b7..a9aec6e8ea4 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -16,7 +16,7 @@
}
Autosave.prototype.restore = function() {
- var e, error, text;
+ var e, text;
if (window.localStorage == null) {
return;
}
@@ -41,7 +41,7 @@
if ((text != null ? text.length : void 0) > 0) {
try {
return window.localStorage.setItem(this.key, text);
- } catch (undefined) {}
+ } catch (error) {}
} else {
return this.reset();
}
@@ -53,7 +53,7 @@
}
try {
return window.localStorage.removeItem(this.key);
- } catch (undefined) {}
+ } catch (error) {}
};
return Autosave;
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index ad12cb906e1..44af1c135a0 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -86,6 +86,8 @@
AwardsHandler.prototype.positionMenu = function($menu, $addBtn) {
var css, position;
position = $addBtn.data('position');
+ // The menu could potentially be off-screen or in a hidden overflow element
+ // So we position the element absolute in the body
css = {
top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px"
};
@@ -255,12 +257,12 @@
};
AwardsHandler.prototype.animateEmoji = function($emoji) {
- var className;
- className = 'pulse animated';
+ var className = 'pulse animated once short';
$emoji.addClass(className);
- return setTimeout((function() {
- return $emoji.removeClass(className);
- }), 321);
+
+ $emoji.on('webkitAnimationEnd animationEnd', function() {
+ $(this).removeClass(className);
+ });
};
AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) {
@@ -284,6 +286,7 @@
if (emojiIcon.length > 0) {
unicodeName = emojiIcon.data('unicode-name');
} else {
+ // Find by alias
unicodeName = $(".emoji-menu-content [data-aliases*=':" + emoji + ":']").data('unicode-name');
}
return "emoji-" + unicodeName;
@@ -350,9 +353,11 @@
return function(ev) {
var found_emojis, h5, term, ul;
term = $(ev.target).val();
+ // Clean previous search results
$('ul.emoji-menu-search, h5.emoji-search').remove();
if (term) {
- h5 = $('<h5>').text('Search results');
+ // Generate a search result block
+ h5 = $('<h5 class="emoji-search" />').text('Search results');
found_emojis = _this.searchEmojis(term).show();
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index f977a1e8a7b..dc8ae601961 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,7 +1,5 @@
/*= require jquery.ba-resize */
-
-
/*= require autosize */
(function() {
diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js
index 3631d1b74ac..1df681a4816 100644
--- a/app/assets/javascripts/behaviors/details_behavior.js
+++ b/app/assets/javascripts/behaviors/details_behavior.js
@@ -5,6 +5,12 @@
container = $(this).closest(".js-details-container");
return container.toggleClass("open");
});
+ // Show details content. Hides link after click.
+ //
+ // %div
+ // %a.js-details-expand
+ // %div.js-details-content
+ //
return $("body").on("click", ".js-details-expand", function(e) {
$(this).next('.js-details-content').removeClass("hide");
$(this).hide();
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 3527d0a95fc..54b7360ab41 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,6 +1,20 @@
-
+// Quick Submit behavior
+//
+// When a child field of a form with a `js-quick-submit` class receives a
+// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
+// is submitted.
+//
/*= require extensions/jquery */
+//
+// ### Example Markup
+//
+// <form action="/foo" class="js-quick-submit">
+// <input type="text" />
+// <textarea></textarea>
+// <input type="submit" value="Submit" />
+// </form>
+//
(function() {
var isMac, keyCodeIs;
@@ -17,6 +31,7 @@
$(document).on('keydown.quick_submit', '.js-quick-submit', function(e) {
var $form, $submit_button;
+ // Enter
if (!keyCodeIs(e, 13)) {
return;
}
@@ -33,8 +48,11 @@
return $form.submit();
});
+ // If the user tabs to a submit button on a `js-quick-submit` form, display a
+ // tooltip to let them know they could've used the hotkey
$(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function(e) {
var $this, title;
+ // Tab
if (!keyCodeIs(e, 9)) {
return;
}
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index db0b36b24e9..894034bdd54 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -1,6 +1,18 @@
-
+// Requires Input behavior
+//
+// When called on a form with input fields with the `required` attribute, the
+// form's submit button will be disabled until all required fields have values.
+//
/*= require extensions/jquery */
+//
+// ### Example Markup
+//
+// <form class="js-requires-input">
+// <input type="text" required="required">
+// <input type="submit" value="Submit">
+// </form>
+//
(function() {
$.fn.requiresInput = function() {
var $button, $form, fieldSelector, requireInput, required;
@@ -11,14 +23,17 @@
requireInput = function() {
var values;
values = _.map($(fieldSelector, $form), function(field) {
+ // Collect the input values of *all* required fields
return field.value;
});
+ // Disable the button if any required fields are empty
if (values.length && _.any(values, _.isEmpty)) {
return $button.disable();
} else {
return $button.enable();
}
};
+ // Set initial button state
requireInput();
return $form.on('change input', fieldSelector, requireInput);
};
@@ -27,6 +42,8 @@
var $form, hideOrShowHelpBlock;
$form = $('form.js-requires-input');
$form.requiresInput();
+ // Hide or Show the help block when creating a new project
+ // based on the option selected
hideOrShowHelpBlock = function(form) {
var selected;
selected = $('.js-select-namespace option:selected');
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 5467e3edc69..a6ce378d67a 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,5 +1,12 @@
(function(w) {
$(function() {
+ // Toggle button. Show/hide content inside parent container.
+ // Button does not change visibility. If button has icon - it changes chevron style.
+ //
+ // %div.js-toggle-container
+ // %a.js-toggle-button
+ // %div.js-toggle-content
+ //
$('body').on('click', '.js-toggle-button', function(e) {
e.preventDefault();
$(this)
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index f4044f22db2..8cca1aa9232 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -8,6 +8,8 @@
autoDiscover: false,
autoProcessQueue: false,
url: form.attr('action'),
+ // Rails uses a hidden input field for PUT
+ // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
method: method,
clickable: true,
uploadMultiple: false,
@@ -36,6 +38,7 @@
formData.append('commit_message', form.find('.js-commit-message').val());
});
},
+ // Override behavior of adding error underneath preview
error: function(file, errorMessage) {
var stripped;
stripped = $("<div/>").html(errorMessage).text();
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index b0a37ef0e0a..95352164d76 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -13,6 +13,9 @@
this.buildDropdown();
this.bindEvents();
this.onFilenameUpdate();
+
+ this.autosizeUpdateEvent = document.createEvent('Event');
+ this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
}
TemplateSelector.prototype.buildDropdown = function() {
@@ -66,9 +69,16 @@
// be added by all subclasses.
};
+ // To be implemented on the extending class
+ // e.g.
+ // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
this.editor.setValue(file.content, 1);
if (!skipFocus) this.editor.focus();
+
+ if (this.editor instanceof jQuery) {
+ this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
+ }
};
TemplateSelector.prototype.startLoadingSpinner = function() {
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 649c79daee8..de6cdd851be 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -18,8 +18,11 @@
return function() {
return $("#file-content").val(_this.editor.getValue());
};
+ // Before a form submission, move the content from the Ace editor into the
+ // submitted textarea
})(this));
this.initModePanesAndLinks();
+ this.initSoftWrap();
new BlobLicenseSelectors({
editor: this.editor
});
@@ -48,6 +51,7 @@
this.$editModePanes.hide();
currentPane.fadeIn(200);
if (paneId === "#preview") {
+ this.$toggleButton.hide();
return $.post(currentLink.data("preview-url"), {
content: this.editor.getValue()
}, function(response) {
@@ -55,10 +59,23 @@
return currentPane.syntaxHighlight();
});
} else {
+ this.$toggleButton.show();
return this.editor.focus();
}
};
+ EditBlob.prototype.initSoftWrap = function() {
+ this.isSoftWrapped = false;
+ this.$toggleButton = $('.soft-wrap-toggle');
+ this.$toggleButton.on('click', this.toggleSoftWrap.bind(this));
+ };
+
+ EditBlob.prototype.toggleSoftWrap = function(e) {
+ this.isSoftWrapped = !this.isSoftWrapped;
+ this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
+ this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
+ };
+
return EditBlob;
})();
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 50fc11d7737..474805c1437 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -34,6 +34,11 @@
},
issues () {
this.$nextTick(() => {
+ if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) {
+ this.list.page++;
+ this.list.getIssues(false);
+ }
+
if (this.scrollHeight() > this.listHeight()) {
this.showCount = true;
} else {
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
index f9f9f7999d4..b5ff3a81ed5 100644
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -1,10 +1,7 @@
-Vue.http.interceptors.push((request, next) => {
+Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
- Vue.nextTick(() => {
- setTimeout(() => {
- Vue.activeResources--;
- }, 500);
+ next(function (response) {
+ Vue.activeResources--;
});
- next();
});
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index 1e0148e5798..5fef9725178 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -23,6 +23,7 @@
if ($(allDeviceSelector.join(",")).length) {
return;
}
+ // Create all the elements
els = $.map(BREAKPOINTS, function(breakpoint) {
return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
});
@@ -40,6 +41,7 @@
BreakpointInstance.prototype.getBreakpointSize = function() {
var $visibleDevice;
$visibleDevice = this.visibleDevice;
+ // the page refreshed via turbolinks
if (!$visibleDevice().length) {
this.setup();
}
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 0d7d29bb0d0..78d21c0552a 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -16,6 +16,7 @@
this.toggleSidebar = bind(this.toggleSidebar, this);
this.updateDropdown = bind(this.updateDropdown, this);
clearInterval(Build.interval);
+ // Init breakpoint checker
this.bp = Breakpoints.get();
$('.js-build-sidebar').niceScroll();
@@ -26,10 +27,11 @@
$(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
$(window).off('resize.build').on('resize.build', this.hideSidebar);
$(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
+ $('#js-build-scroll > a').off('click').on('click', this.stepTrace);
this.updateArtifactRemoveDate();
if ($('#build-trace').length) {
this.getInitialBuildTrace();
- this.initScrollButtonAffix();
+ this.initScrollButtons();
}
if (this.build_status === "running" || this.build_status === "pending") {
$('#autoscroll-button').on('click', function() {
@@ -42,6 +44,9 @@
$(this).data("state", "enabled");
return $(this).text("disable autoscroll");
}
+ //
+ // Bind autoscroll button to follow build output
+ //
});
Build.interval = setInterval((function(_this) {
return function() {
@@ -49,17 +54,23 @@
return _this.getBuildTrace();
}
};
+ //
+ // Check for new build output if user still watching build page
+ // Only valid for runnig build when output changes during time
+ //
})(this), 4000);
}
}
Build.prototype.getInitialBuildTrace = function() {
+ var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
+
return $.ajax({
url: this.build_url,
dataType: 'json',
success: function(build_data) {
$('.js-build-output').html(build_data.trace_html);
- if (build_data.status === 'success' || build_data.status === 'failed') {
+ if (removeRefreshStatuses.indexOf(build_data.status) >= 0) {
return $('.js-build-refresh').remove();
}
}
@@ -96,7 +107,7 @@
}
};
- Build.prototype.initScrollButtonAffix = function() {
+ Build.prototype.initScrollButtons = function() {
var $body, $buildScroll, $buildTrace;
$buildScroll = $('#js-build-scroll');
$body = $('body');
@@ -155,6 +166,14 @@
this.populateJobs(stage);
};
+ Build.prototype.stepTrace = function(e) {
+ e.preventDefault();
+ $currentTarget = $(e.currentTarget);
+ $.scrollTo($currentTarget.attr('href'), {
+ offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
+ });
+ };
+
return Build;
})();
diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6
new file mode 100644
index 00000000000..8d3e29794a1
--- /dev/null
+++ b/app/assets/javascripts/build_variables.js.es6
@@ -0,0 +1,6 @@
+$(function(){
+ $('.reveal-variables').off('click').on('click',function(){
+ $('.js-build').toggle().niceScroll();
+ $(this).hide();
+ });
+});
diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image-file.js
index c0d0b2d049f..e893491b19b 100644
--- a/app/assets/javascripts/commit/image-file.js
+++ b/app/assets/javascripts/commit/image-file.js
@@ -2,6 +2,7 @@
this.ImageFile = (function() {
var prepareFrames;
+ // Width where images must fits in, for 2-up this gets divided by 2
ImageFile.availWidth = 900;
ImageFile.viewModes = ['two-up', 'swipe'];
@@ -9,6 +10,7 @@
function ImageFile(file) {
this.file = file;
this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) {
+ // Determine if old and new file has same dimensions, if not show 'two-up' view
return function(deletedWidth, deletedHeight) {
return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) {
if (width === deletedWidth && height === deletedHeight) {
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 37f168c5190..9132089adcd 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -45,6 +45,7 @@
CommitsList.content.html(data.html);
return history.replaceState({
page: commitsUrl
+ // Change url so if user reload a page - search results are saved
}, document.title, commitsUrl);
},
dataType: "json"
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 4e3a28cd163..294d2c9052c 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -23,8 +23,9 @@
selectable: true,
filterable: true,
filterByText: true,
- fieldName: $dropdown.attr('name'),
- filterInput: 'input[type="text"]',
+ toggleLabel: true,
+ fieldName: $dropdown.data('field-name'),
+ filterInput: 'input[type="search"]',
renderRow: function(ref) {
var link;
if (ref.header != null) {
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index c43af17442b..3e20db7e308 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -6,14 +6,19 @@
genericSuccess = function(e) {
showTooltip(e.trigger, 'Copied!');
+ // Clear the selection and blur the trigger so it loses its border
e.clearSelection();
return $(e.trigger).blur();
};
+ // Safari doesn't support `execCommand`, so instead we inform the user to
+ // copy manually.
+ //
+ // See http://clipboardjs.com/#browser-support
genericError = function(e) {
var key;
if (/Mac/i.test(navigator.userAgent)) {
- key = '&#8984;';
+ key = '&#8984;'; // Command
} else {
key = 'Ctrl';
}
diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle-analytics.js.es6
new file mode 100644
index 00000000000..cd9886ba58d
--- /dev/null
+++ b/app/assets/javascripts/cycle-analytics.js.es6
@@ -0,0 +1,93 @@
+((global) => {
+
+ const COOKIE_NAME = 'cycle_analytics_help_dismissed';
+ const store = gl.cycleAnalyticsStore = {
+ isLoading: true,
+ hasError: false,
+ isHelpDismissed: $.cookie(COOKIE_NAME),
+ analytics: {}
+ };
+
+ gl.CycleAnalytics = class CycleAnalytics {
+ constructor() {
+ const that = this;
+
+ this.vue = new Vue({
+ el: '#cycle-analytics',
+ name: 'CycleAnalytics',
+ created: this.fetchData(),
+ data: store,
+ methods: {
+ dismissLanding() {
+ that.dismissLanding();
+ }
+ }
+ });
+ }
+
+ fetchData(options) {
+ store.isLoading = true;
+ options = options || { startDate: 30 };
+
+ $.ajax({
+ url: $('#cycle-analytics').data('request-path'),
+ method: 'GET',
+ dataType: 'json',
+ contentType: 'application/json',
+ data: { start_date: options.startDate }
+ }).done((data) => {
+ this.decorateData(data);
+ this.initDropdown();
+ })
+ .error((data) => {
+ this.handleError(data);
+ })
+ .always(() => {
+ store.isLoading = false;
+ })
+ }
+
+ decorateData(data) {
+ data.summary = data.summary || [];
+ data.stats = data.stats || [];
+
+ data.summary.forEach((item) => {
+ item.value = item.value || '-';
+ });
+
+ data.stats.forEach((item) => {
+ item.value = item.value || '- - -';
+ });
+
+ store.analytics = data;
+ }
+
+ handleError(data) {
+ store.hasError = true;
+ new Flash('There was an error while fetching cycle analytics data.', 'alert');
+ }
+
+ dismissLanding() {
+ store.isHelpDismissed = true;
+ $.cookie(COOKIE_NAME, true, {
+ path: gon.relative_url_root || '/'
+ });
+ }
+
+ initDropdown() {
+ const $dropdown = $('.js-ca-dropdown');
+ const $label = $dropdown.find('.dropdown-label');
+
+ $dropdown.find('li a').off('click').on('click', (e) => {
+ e.preventDefault();
+ const $target = $(e.currentTarget);
+ const value = $target.data('value');
+
+ $label.text($target.text().trim());
+ this.fetchData({ startDate: value });
+ })
+ }
+
+ }
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 3dd7ceba92f..c8634b78f2b 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -39,6 +39,9 @@
bottom: unfoldBottom,
offset: offset,
unfold: unfold,
+ // indent is used to compensate for single space indent to fit
+ // '+' and '-' prepended to diff lines,
+ // see https://gitlab.com/gitlab-org/gitlab-ce/issues/707
indent: 1,
view: file.data('view')
};
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
index be6ebc77947..cdedfd1af15 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -1,13 +1,9 @@
((w) => {
w.ResolveBtn = Vue.extend({
- mixins: [
- ButtonMixins
- ],
props: {
noteId: Number,
discussionId: String,
resolved: Boolean,
- namespacePath: String,
projectPath: String,
canResolve: Boolean,
resolvedBy: String
@@ -69,10 +65,10 @@
if (this.isResolved) {
promise = ResolveService
- .unresolve(this.namespace, this.noteId);
+ .unresolve(this.projectPath, this.noteId);
} else {
promise = ResolveService
- .resolve(this.namespace, this.noteId);
+ .resolve(this.projectPath, this.noteId);
}
promise.then((response) => {
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
index e373b06b1eb..0a617034502 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -1,12 +1,8 @@
((w) => {
w.ResolveDiscussionBtn = Vue.extend({
- mixins: [
- ButtonMixins
- ],
props: {
discussionId: String,
mergeRequestId: Number,
- namespacePath: String,
projectPath: String,
canResolve: Boolean,
},
@@ -50,7 +46,7 @@
},
methods: {
resolve: function () {
- ResolveService.toggleResolveForDiscussion(this.namespace, this.mergeRequestId, this.discussionId);
+ ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId);
}
},
created: function () {
diff --git a/app/assets/javascripts/diff_notes/mixins/namespace.js.es6 b/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
deleted file mode 100644
index d278678085b..00000000000
--- a/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
+++ /dev/null
@@ -1,9 +0,0 @@
-((w) => {
- w.ButtonMixins = {
- computed: {
- namespace: function () {
- return `${this.namespacePath}/${this.projectPath}`;
- }
- }
- };
-})(window);
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
index de771ff814b..2a55f739b31 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -9,32 +9,32 @@
Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
}
- prepareRequest(namespace) {
+ prepareRequest(root) {
this.setCSRF();
- Vue.http.options.root = `/${namespace}`;
+ Vue.http.options.root = root;
}
- resolve(namespace, noteId) {
- this.prepareRequest(namespace);
+ resolve(projectPath, noteId) {
+ this.prepareRequest(projectPath);
return this.noteResource.save({ noteId }, {});
}
- unresolve(namespace, noteId) {
- this.prepareRequest(namespace);
+ unresolve(projectPath, noteId) {
+ this.prepareRequest(projectPath);
return this.noteResource.delete({ noteId }, {});
}
- toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) {
+ toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId],
isResolved = discussion.isResolved();
let promise;
if (isResolved) {
- promise = this.unResolveAll(namespace, mergeRequestId, discussionId);
+ promise = this.unResolveAll(projectPath, mergeRequestId, discussionId);
} else {
- promise = this.resolveAll(namespace, mergeRequestId, discussionId);
+ promise = this.resolveAll(projectPath, mergeRequestId, discussionId);
}
promise.then((response) => {
@@ -57,10 +57,10 @@
})
}
- resolveAll(namespace, mergeRequestId, discussionId) {
+ resolveAll(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
- this.prepareRequest(namespace);
+ this.prepareRequest(projectPath);
discussion.loading = true;
@@ -70,10 +70,10 @@
}, {});
}
- unResolveAll(namespace, mergeRequestId, discussionId) {
+ unResolveAll(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
- this.prepareRequest(namespace);
+ this.prepareRequest(projectPath);
discussion.loading = true;
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index a72beb42646..aedffeea2a2 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -23,6 +23,7 @@
case 'projects:boards:show':
shortcut_handler = new ShortcutsNavigation();
break;
+ case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
new IssuableBulkActions();
@@ -97,6 +98,7 @@
break;
case "projects:merge_requests:conflicts":
window.mcui = new MergeConflictResolver()
+ break;
case 'projects:merge_requests:index':
shortcut_handler = new ShortcutsNavigation();
Issuable.init();
@@ -171,6 +173,8 @@
}
break;
case 'projects:network:show':
+ // Ensure we don't create a particular shortcut handler here. This is
+ // already created, where the network graph is created.
shortcut_handler = true;
break;
case 'projects:forks:new':
@@ -190,6 +194,9 @@
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
break;
+ case 'projects:cycle_analytics:show':
+ new gl.CycleAnalytics();
+ break;
}
switch (path.first()) {
case 'admin':
@@ -267,12 +274,14 @@
shortcut_handler = new ShortcutsNavigation();
}
}
+ // If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
return new Shortcuts();
}
};
Dispatcher.prototype.initSearch = function() {
+ // Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
}
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 5a725a41fd1..bf68b7e3a9b 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -2,6 +2,7 @@
this.DueDateSelect = (function() {
function DueDateSelect() {
var $datePicker, $dueDate, $loading;
+ // Milestone edit/new form
$datePicker = $('.datepicker');
if ($datePicker.length) {
$dueDate = $('#milestone_due_date');
@@ -16,6 +17,7 @@
e.preventDefault();
return $.datepicker._clearDate($datePicker);
});
+ // Issuable sidebar
$loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
$('.js-due-date-select').each(function(i, dropdown) {
var $block, $dropdown, $dropdownParent, $selectbox, $sidebarValue, $value, $valueContent, abilityName, addDueDate, fieldName, issueUpdateURL;
@@ -38,6 +40,7 @@
});
addDueDate = function(isDropdown) {
var data, date, mediumDate, value;
+ // Create the post date
value = $("input[name='" + fieldName + "']").val();
if (value !== '') {
date = new Date(value.replace(new RegExp('-', 'g'), ','));
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
index ae3dde63da3..4978e24949c 100644
--- a/app/assets/javascripts/extensions/jquery.js
+++ b/app/assets/javascripts/extensions/jquery.js
@@ -1,3 +1,4 @@
+// Disable an element and add the 'disabled' Bootstrap class
(function() {
$.fn.extend({
disable: function() {
@@ -5,6 +6,7 @@
}
});
+ // Enable an element and remove the 'disabled' Bootstrap class
$.fn.extend({
enable: function() {
return $(this).removeAttr('disabled').removeClass('disabled');
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 3dca06d36b1..d0786bf0053 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -1,3 +1,4 @@
+// Creates the variables for setting up GFM auto-completion
(function() {
if (window.GitLab == null) {
window.GitLab = {};
@@ -8,18 +9,22 @@
dataLoaded: false,
cachedData: {},
dataSource: '',
+ // Emoji
Emoji: {
template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
},
+ // Team Members
Members: {
template: '<li>${username} <small>${title}</small></li>'
},
Labels: {
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
},
+ // Issues and MergeRequests
Issues: {
template: '<li><small>${id}</small> ${title}</li>'
},
+ // Milestones
Milestones: {
template: '<li>${title}</li>'
},
@@ -48,8 +53,11 @@
}
},
setup: function(input) {
+ // Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
+ // destroy previous instances
this.destroyAtWho();
+ // set up instances
this.setupAtWho();
if (this.dataSource) {
if (!this.dataLoading && !this.cachedData) {
@@ -63,6 +71,11 @@
return _this.loadData(data);
});
};
+ // We should wait until initializations are done
+ // and only trigger the last .setup since
+ // The previous .dataSource belongs to the previous issuable
+ // and the last one will have the **proper** .dataSource property
+ // TODO: Make this a singleton and turn off events when moving to another page
})(this), 1000);
}
if (this.cachedData != null) {
@@ -71,6 +84,7 @@
}
},
setupAtWho: function() {
+ // Emoji
this.input.atwho({
at: ':',
displayTpl: (function(_this) {
@@ -90,6 +104,7 @@
beforeInsert: this.DefaultOptions.beforeInsert
}
});
+ // Team Members
this.input.atwho({
at: '@',
displayTpl: (function(_this) {
@@ -321,13 +336,22 @@
loadData: function(data) {
this.cachedData = data;
this.dataLoaded = true;
+ // load members
this.input.atwho('load', '@', data.members);
+ // load issues
this.input.atwho('load', 'issues', data.issues);
+ // load milestones
this.input.atwho('load', 'milestones', data.milestones);
+ // load merge requests
this.input.atwho('load', 'mergerequests', data.mergerequests);
+ // load emojis
this.input.atwho('load', ':', data.emojis);
+ // load labels
this.input.atwho('load', '~', data.labels);
+ // load commands
this.input.atwho('load', '/', data.commands);
+ // This trigger at.js again
+ // otherwise we would be stuck with loading until the user types
return $(':focus').trigger('keyup');
}
};
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 77b2082cba0..1b6db641200 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -21,12 +21,14 @@
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
this.indeterminateIds = [];
$clearButton.on('click', (function(_this) {
+ // Clear click
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.input.val('').trigger('keyup').focus();
};
})(this));
+ // Key events
timeout = "";
this.input
.on('keydown', function (e) {
@@ -49,6 +51,7 @@
if (keyCode === 13 && !options.elIsInput) {
return false;
}
+ // Only filter asynchronously only if option remote is set
if (this.options.remote) {
clearTimeout(timeout);
return timeout = setTimeout(function() {
@@ -79,11 +82,27 @@
if ((data != null) && !this.options.filterByText) {
results = data;
if (search_text !== '') {
+ // When data is an array of objects therefore [object Array] e.g.
+ // [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ]
if (_.isArray(data)) {
results = fuzzaldrinPlus.filter(data, search_text, {
key: this.options.keys
});
} else {
+ // If data is grouped therefore an [object Object]. e.g.
+ // {
+ // groupName1: [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ],
+ // groupName2: [
+ // { prop: 'abc' },
+ // { prop: 'def' }
+ // ]
+ // }
if (gl.utils.isObject(data)) {
results = {};
for (key in data) {
@@ -140,6 +159,7 @@
this.options.beforeSend();
}
return this.dataEndpoint("", (function(_this) {
+ // Fetch the data by calling the data funcfion
return function(data) {
if (_this.options.success) {
_this.options.success(data);
@@ -171,6 +191,7 @@
};
})(this)
});
+ // Fetch the data through ajax if the data is a string
};
return GitLabDropdownRemote;
@@ -209,13 +230,18 @@
self = this;
selector = $(this.el).data("target");
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
+ // Set Defaults
ref = this.options, this.filterInput = (ref1 = ref.filterInput) != null ? ref1 : this.getElement(FILTER_INPUT), this.highlight = (ref2 = ref.highlight) != null ? ref2 : false, this.filterInputBlur = (ref3 = ref.filterInputBlur) != null ? ref3 : true;
+ // If no input is passed create a default one
self = this;
+ // If selector was passed
if (_.isString(this.filterInput)) {
this.filterInput = this.getElement(this.filterInput);
}
searchFields = this.options.search ? this.options.search.fields : [];
if (this.options.data) {
+ // If we provided data
+ // data could be an array of objects or a group of arrays
if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
this.fullData = this.options.data;
currentIndex = -1;
@@ -232,10 +258,12 @@
return _this.filter.input.trigger('keyup');
}
};
+ // Remote data
})(this)
});
}
}
+ // Init filterable
if (this.options.filterable) {
this.filter = new GitLabDropdownFilter(this.filterInput, {
elIsInput: $(this.el).is('input'),
@@ -278,12 +306,14 @@
})(this)
});
}
+ // Event listeners
this.dropdown.on("shown.bs.dropdown", this.opened);
this.dropdown.on("hidden.bs.dropdown", this.hidden);
$(this.el).on("update.label", this.updateLabel);
this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate);
this.dropdown.on('keyup', (function(_this) {
return function(e) {
+ // Escape key
if (e.which === 27) {
return $('.dropdown-menu-close', _this.dropdown).trigger('click');
}
@@ -322,11 +352,18 @@
if (self.options.clicked) {
self.options.clicked(selected, $el, e);
}
- return $el.trigger('blur');
+
+ // Update label right after all modifications in dropdown has been done
+ if (self.options.toggleLabel) {
+ self.updateLabel(selected, $el, self);
+ }
+
+ $el.trigger('blur');
});
}
}
+ // Finds an element inside wrapper element
GitLabDropdown.prototype.getElement = function(selector) {
return this.dropdown.find(selector);
};
@@ -344,6 +381,7 @@
}
}
menu.toggleClass(PAGE_TWO_CLASS);
+ // Focus first visible input on active page
return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
};
@@ -351,23 +389,28 @@
var full_html, groupData, html, name;
this.renderedData = data;
if (this.options.filterable && data.length === 0) {
+ // render no matching results
html = [this.noResults()];
} else {
+ // Handle array groups
if (gl.utils.isObject(data)) {
html = [];
for (name in data) {
groupData = data[name];
html.push(this.renderItem({
header: name
+ // Add header for each group
}, name));
this.renderData(groupData, name).map(function(item) {
return html.push(item);
});
}
} else {
+ // Render each row
html = this.renderData(data);
}
}
+ // Render the full menu
full_html = this.renderMenu(html);
return this.appendMenu(full_html);
};
@@ -406,6 +449,7 @@
if (this.options.setActiveIds) {
this.options.setActiveIds.call(this);
}
+ // Makes indeterminate items effective
if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
this.parseData(this.fullData);
}
@@ -427,6 +471,8 @@
if (this.options.filterable) {
$input.blur().val("");
}
+ // Triggering 'keyup' will re-render the dropdown which is not always required
+ // specially if we want to keep the state of the dropdown needed for bulk-assignment
if (!this.options.persistWhenHide) {
$input.trigger("keyup");
}
@@ -439,6 +485,7 @@
return this.dropdown.trigger('hidden.gl.dropdown');
};
+ // Render the full menu
GitLabDropdown.prototype.renderMenu = function(html) {
var menu_html;
menu_html = "";
@@ -450,6 +497,7 @@
return menu_html;
};
+ // Append the menu into the dropdown
GitLabDropdown.prototype.appendMenu = function(html) {
var selector;
selector = '.dropdown-content';
@@ -465,35 +513,42 @@
group = false;
}
if (index == null) {
+ // Render the row
index = false;
}
html = "";
+ // Divider
if (data === "divider") {
return "<li class='divider'></li>";
}
+ // Separator is a full-width divider
if (data === "separator") {
return "<li class='separator'></li>";
}
+ // Header
if (data.header != null) {
return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header });
}
if (this.options.renderRow) {
+ // Call the render function
html = this.options.renderRow.call(this.options, data, this);
} else {
if (!selected) {
value = this.options.id ? this.options.id(data) : data.id;
- fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName() : this.options.fieldName;
+ fieldName = this.options.fieldName;
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) {
selected = true;
}
}
+ // Set URL
if (this.options.url != null) {
url = this.options.url(data);
} else {
url = data.url != null ? data.url : '#';
}
+ // Set Text
if (this.options.text != null) {
text = this.options.text(data);
} else {
@@ -540,6 +595,7 @@
GitLabDropdown.prototype.rowClicked = function(el) {
var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
+ fieldName = this.options.fieldName;
isInput = $(this.el).is('input');
if (this.renderedData) {
groupName = el.data('group');
@@ -551,27 +607,29 @@
selectedObject = this.renderedData[selectedIndex];
}
}
- fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName;
+ field = [];
value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) {
field = $(this.el);
- } else {
- field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + escape(value) + "']");
+ } else if(value) {
+ field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS);
- if (isInput) {
- field.val('');
- } else {
- field.remove();
+ if (field && field.length) {
+ if (isInput) {
+ field.val('');
+ } else {
+ field.remove();
+ }
}
} else if (el.hasClass(INDETERMINATE_CLASS)) {
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
- if (value == null) {
+ if (field && field.length && value == null) {
field.remove();
}
- if (!field.length && fieldName) {
+ if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject);
}
} else {
@@ -581,36 +639,30 @@
this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
}
}
- if (value == null) {
+ if (field && field.length && value == null) {
field.remove();
}
+ // Toggle active class for the tick mark
el.addClass(ACTIVE_CLASS);
if (value != null) {
- if (!field.length && fieldName) {
+ if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject);
- } else {
+ } else if (field && field.length) {
field.val(value).trigger('change');
}
}
}
- // Update label right after input has been added
- if (this.options.toggleLabel) {
- this.updateLabel(selectedObject, el, this);
- }
-
return selectedObject;
};
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
var $input;
+ // Create hidden input for form
$input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
}
- if (selectedObject && selectedObject.type) {
- $input.attr('data-type', selectedObject.type);
- }
return this.dropdown.before($input);
};
@@ -625,6 +677,7 @@
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
+ // simulate a click on the first link
$el = $(selector, this.dropdown);
if ($el.length) {
var href = $el.attr('href');
@@ -653,11 +706,15 @@
e.stopImmediatePropagation();
PREV_INDEX = currentIndex;
$listItems = $(selector, _this.dropdown);
+ // if @options.filterable
+ // $input.blur()
if (currentKeyCode === 40) {
+ // Move down
if (currentIndex < ($listItems.length - 1)) {
currentIndex += 1;
}
} else if (currentKeyCode === 38) {
+ // Move up
if (currentIndex > 0) {
currentIndex -= 1;
}
@@ -685,24 +742,32 @@
GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
+ // Remove the class for the previously focused row
$('.is-focused', this.dropdown).removeClass('is-focused');
+ // Update the class for the row at the specific index
$listItem = $listItems.eq(index);
$listItem.find('a:first-child').addClass("is-focused");
+ // Dropdown content scroll area
$dropdownContent = $listItem.closest('.dropdown-content');
dropdownScrollTop = $dropdownContent.scrollTop();
dropdownContentHeight = $dropdownContent.outerHeight();
dropdownContentTop = $dropdownContent.prop('offsetTop');
dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
+ // Get the offset bottom of the list item
listItemHeight = $listItem.outerHeight();
listItemTop = $listItem.prop('offsetTop');
listItemBottom = listItemTop + listItemHeight;
if (!index) {
+ // Scroll the dropdown content to the top
$dropdownContent.scrollTop(0)
} else if (index === ($listItems.length - 1)) {
+ // Scroll the dropdown content to the bottom
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
} else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
+ // Scroll the dropdown content down
$dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
} else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
+ // Scroll the dropdown content up
return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
}
};
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 528a673eb15..2703adc0705 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -3,12 +3,15 @@
function GLForm(form) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
+ // Before we start, we should clean up any previous data for this form
this.destroy();
+ // Setup the form
this.setupForm();
this.form.data('gl-form', this);
}
GLForm.prototype.destroy = function() {
+ // Clean form listeners
this.clearEventListeners();
return this.form.data('gl-form', null);
};
@@ -21,12 +24,15 @@
this.form.find('.div-dropzone').remove();
this.form.addClass('gfm-form');
disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
+ // remove notify commit author checkbox for non-commit notes
GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
+ // form and textarea event listeners
this.addEventListeners();
gl.text.init(this.form);
}
+ // hide discard button
this.form.find('.js-note-discard').hide();
return this.form.show();
};
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index b95faadc8e7..4886da9f21f 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,7 +1,11 @@
-
+// This is a manifest file that'll be compiled into including all the files listed below.
+// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+// be included in the compiled file accessible from http://example.com/assets/application.js
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// the compiled file.
+//
/*= require_tree . */
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index a646ca1d84f..7d9d4d7c679 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -204,6 +204,7 @@
function ContributorsAuthorGraph(data1) {
this.data = data1;
+ // Don't split graph size in half for mobile devices.
if ($(window).width() < 768) {
this.width = $('.content').width() - 80;
} else {
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index fd5b6dc0ddd..7c2eebcdd44 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -38,6 +38,7 @@
return _this.formatSelection.apply(_this, args);
},
dropdownCssClass: "ajax-groups-dropdown",
+ // we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 0f840821f53..4aced1e618f 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -10,21 +10,24 @@
ImporterStatus.prototype.initStatusPage = function() {
$('.js-add-to-import').off('click').on('click', (function(_this) {
return function(e) {
- var $btn, $namespace_input, $target_field, $tr, id, new_namespace;
+ var $btn, $namespace_input, $target_field, $tr, id, target_namespace, newName;
$btn = $(e.currentTarget);
$tr = $btn.closest('tr');
$target_field = $tr.find('.import-target');
- $namespace_input = $target_field.find('input');
+ $namespace_input = $target_field.find('.js-select-namespace option:selected');
id = $tr.attr('id').replace('repo_', '');
- new_namespace = null;
+ target_namespace = null;
+ newName = null;
if ($namespace_input.length > 0) {
- new_namespace = $namespace_input.prop('value');
- $target_field.empty().append(new_namespace + "/" + ($target_field.data('project_name')));
+ target_namespace = $namespace_input[0].innerHTML;
+ newName = $target_field.find('#path').prop('value');
+ $target_field.empty().append(target_namespace + "/" + newName);
}
$btn.disable().addClass('is-loading');
return $.post(_this.import_url, {
repo_id: id,
- new_namespace: new_namespace
+ target_namespace: target_namespace,
+ new_name: newName
}, {
dataType: 'script'
});
@@ -70,7 +73,7 @@
if ($('.js-importer-status').length) {
var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
var importPath = $('.js-importer-status').data('import-path');
-
+
new ImporterStatus(jobsImportPath, importPath);
}
});
diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js.es6
index 86ecad0b1fc..57f7e4ef230 100644
--- a/app/assets/javascripts/issuable.js
+++ b/app/assets/javascripts/issuable.js.es6
@@ -8,38 +8,48 @@
Issuable.initTemplates();
Issuable.initSearch();
Issuable.initChecks();
+ Issuable.initResetFilters();
return Issuable.initLabelFilterRemove();
},
initTemplates: function() {
return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
},
initSearch: function() {
- this.timer = null;
- return $('#issue_search').off('keyup').on('keyup', function() {
- clearTimeout(this.timer);
- return this.timer = setTimeout(function() {
- var $form, $input, $search;
- $search = $('#issue_search');
- $form = $('.js-filter-form');
- $input = $("input[name='" + ($search.attr('name')) + "']", $form);
- if ($input.length === 0) {
- $form.append("<input type='hidden' name='" + ($search.attr('name')) + "' value='" + (_.escape($search.val())) + "'/>");
- } else {
- $input.val($search.val());
- }
- if ($search.val() !== '') {
- return Issuable.filterResults($form);
- }
- }, 500);
+ // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
+ const debouncedExecSearch = _.debounce(Issuable.executeSearch, 500, false);
+
+ $('#issuable_search').off('keyup').on('keyup', debouncedExecSearch);
+
+ // ensures existing filters are preserved when manually submitted
+ $('#issue_search_form').on('submit', (e) => {
+ e.preventDefault();
+ debouncedExecSearch(e);
});
},
+ executeSearch: function(e) {
+ const $search = $('#issuable_search');
+ const $searchName = $search.attr('name');
+ const $searchValue = $search.val();
+ const $filtersForm = $('.js-filter-form');
+ const $input = $(`input[name='${$searchName}']`, $filtersForm);
+
+ if (!$input.length) {
+ $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
+ } else {
+ $input.val($searchValue);
+ }
+
+ Issuable.filterResults($filtersForm);
+ },
initLabelFilterRemove: function() {
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
var $button;
$button = $(this);
+ // Remove the label input box
$('input[name="label_name[]"]').filter(function() {
return this.value === $button.data('label');
}).remove();
+ // Submit the form to get new data
Issuable.filterResults($('.filter-form'));
});
},
@@ -54,6 +64,17 @@
return Turbolinks.visit(issuesUrl);
};
})(this),
+ initResetFilters: function() {
+ $('.reset-filters').on('click', function(e) {
+ e.preventDefault();
+ const target = e.target;
+ const $form = $(target).parents('.js-filter-form');
+ const baseIssuesUrl = target.href;
+
+ $form.attr('action', baseIssuesUrl);
+ Turbolinks.visit(baseIssuesUrl);
+ });
+ },
initChecks: function() {
this.issuableBulkActions = $('.bulk-update').data('bulkActions');
$('.check_all_issues').off('click').on('click', function() {
@@ -63,19 +84,22 @@
return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
},
checkChanged: function() {
- var checked_issues, ids;
- checked_issues = $('.selected_issue:checked');
- if (checked_issues.length > 0) {
- ids = $.map(checked_issues, function(value) {
+ const $checkedIssues = $('.selected_issue:checked');
+ const $updateIssuesIds = $('#update_issuable_ids');
+ const $issuesOtherFilters = $('.issues-other-filters');
+ const $issuesBulkUpdate = $('.issues_bulk_update');
+
+ if ($checkedIssues.length > 0) {
+ let ids = $.map($checkedIssues, function(value) {
return $(value).data('id');
});
- $('#update_issues_ids').val(ids);
- $('.issues-other-filters').hide();
- $('.issues_bulk_update').show();
+ $updateIssuesIds.val(ids);
+ $issuesOtherFilters.hide();
+ $issuesBulkUpdate.show();
} else {
- $('#update_issues_ids').val([]);
- $('.issues_bulk_update').hide();
- $('.issues-other-filters').show();
+ $updateIssuesIds.val([]);
+ $issuesBulkUpdate.hide();
+ $issuesOtherFilters.show();
this.issuableBulkActions.willUpdateLabels = false;
}
return true;
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 6838d9d8da1..261bf6137c2 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,10 +1,6 @@
/*= require flash */
-
-
/*= require jquery.waitforimages */
-
-
/*= require task_list */
(function() {
@@ -13,6 +9,7 @@
this.Issue = (function() {
function Issue() {
this.submitNoteForm = bind(this.submitNoteForm, this);
+ // Prevent duplicate event bindings
this.disableTaskList();
if ($('a.btn-close').length) {
this.initTaskList();
@@ -99,6 +96,8 @@
url: $('form.js-issuable-update').attr('action'),
data: patchData
});
+ // TODO (rspeicher): Make the issue description inline-editable like a note so
+ // that we can re-use its form here
};
Issue.prototype.initMergeRequests = function() {
@@ -127,7 +126,9 @@
Issue.prototype.initCanCreateBranch = function() {
var $container;
- $container = $('div#new-branch');
+ $container = $('#new-branch');
+ // If the user doesn't have the required permissions the container isn't
+ // rendered at all.
if ($container.length === 0) {
return;
}
@@ -139,7 +140,6 @@
if (data.can_create_branch) {
$container.find('.checking').hide();
$container.find('.available').show();
- return $container.find('a').attr('disabled', false);
} else {
$container.find('.checking').hide();
return $container.find('.unavailable').show();
diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues-bulk-assignment.js
index 98d3358ba92..62a7fc9a06c 100644
--- a/app/assets/javascripts/issues-bulk-assignment.js
+++ b/app/assets/javascripts/issues-bulk-assignment.js
@@ -1,14 +1,17 @@
(function() {
this.IssuableBulkActions = (function() {
function IssuableBulkActions(opts) {
+ // Set defaults
var ref, ref1, ref2;
if (opts == null) {
opts = {};
}
- this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issues-list .issue');
+ this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li');
+ // Save instance
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
this.bindEvents();
+ // Fixes bulk-assign not working when navigating through pages
Issuable.initChecks();
}
@@ -86,6 +89,7 @@
ref1 = this.getLabelsFromSelection();
for (j = 0, len1 = ref1.length; j < len1; j++) {
id = ref1[j];
+ // Only the ones that we are not going to keep
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
@@ -106,7 +110,7 @@
state_event: this.form.find('input[name="update[state_event]"]').val(),
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
- issues_ids: this.form.find('input[name="update[issues_ids]"]').val(),
+ issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
add_label_ids: [],
remove_label_ids: []
@@ -147,6 +151,8 @@
indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
labelsToApply = this.getLabelsToApply();
indeterminatedLabels.map(function(id) {
+ // We need to exclude label IDs that will be applied
+ // By not doing this will cause issues from selection to not add labels at all
if (labelsToApply.indexOf(id) === -1) {
return result.push(id);
}
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
index fe071fca67c..cb16e2ba814 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels.js
@@ -26,13 +26,16 @@
var previewColor;
previewColor = $('input#label_color').val();
return $('div.label-color-preview').css('background-color', previewColor);
+ // Updates the the preview color with the hex-color input
};
+ // Updates the preview color with a click on a suggested color
Labels.prototype.setSuggestedColor = function(e) {
var color;
color = $(e.currentTarget).data('color');
$('input#label_color').val(color);
this.updateColorPreview();
+ // Notify the form, that color has changed
$('.label-form').trigger('keyup');
return e.preventDefault();
};
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 9d40a303a8f..2324c166a79 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,7 +4,7 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, $toggleText, fieldName, useId, propertyName;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName;
$dropdown = $(dropdown);
$toggleText = $dropdown.find('.dropdown-toggle-text');
projectId = $dropdown.data('project-id');
@@ -28,7 +28,11 @@
fieldName = $dropdown.data('field-name');
useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
propertyName = useId ? 'id' : 'title';
-
+ initialSelected = $selectbox
+ .find('input[name="' + $dropdown.data('field-name') + '"]')
+ .map(function () {
+ return this.value;
+ }).get();
if (issueUpdateURL != null) {
issueURLSplit = issueUpdateURL.split('/');
}
@@ -48,6 +52,10 @@
selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
return this.value;
}).get();
+
+ if (_.isEqual(initialSelected, selected)) return;
+ initialSelected = selected;
+
data = {};
data[abilityName] = {};
data[abilityName].label_ids = selected;
@@ -163,15 +171,17 @@
selectedClass.push('is-indeterminate');
}
if (active.indexOf(label.id) !== -1) {
+ // Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf('is-indeterminate');
if (i !== -1) {
selectedClass.splice(i, 1);
}
selectedClass.push('is-active');
+ // Add input manually
instance.addInput(this.fieldName, label.id);
}
}
- if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + escape(this.id(label)) + "']").length) {
+ if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) {
selectedClass.push('is-active');
}
if ($dropdown.hasClass('js-multiselect') && removesAll) {
@@ -179,6 +189,7 @@
}
if (label.duplicate) {
spacing = 100 / label.color.length;
+ // Reduce the colors to 4
label.color = label.color.filter(function(color, i) {
return i < 4;
});
@@ -199,11 +210,13 @@
} else {
colorEl = '';
}
+ // We need to identify which items are actually labels
if (label.id) {
selectedClass.push('label-item');
$a.attr('data-label-id', label.id);
}
$a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title);
+ // Return generated html
return $li.html($a).prop('outerHTML');
},
persistWhenHide: $dropdown.data('persistWhenHide'),
@@ -254,6 +267,7 @@
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
$selectbox.hide();
+ // display:block overrides the hide-collapse rule
$value.removeAttr('style');
if ($dropdown.hasClass('js-issuable-form-dropdown')) {
@@ -276,6 +290,7 @@
}
}
if ($dropdown.hasClass('js-filter-bulk-update')) {
+ // If we are persisting state we need the classes
if (!this.options.persistWhenHide) {
return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass();
}
@@ -302,12 +317,12 @@
if (page === 'projects:boards:show') {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
- } else if (label.title) {
+ } else if ($el.hasClass('is-active')) {
gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
} else {
var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
- filters = filters.filter(function (label) {
- return label !== $el.text().trim();
+ filters = filters.filter(function (filteredLabel) {
+ return filteredLabel !== label.title;
});
gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
}
@@ -353,7 +368,9 @@
if ($('.selected_issue:checked').length) {
return;
}
+ // Remove inputs
$('.issues_bulk_update .labels-filter input[type="hidden"]').remove();
+ // Also restore button text
return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
};
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index ce472f3bcd0..8e2fc0d1479 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -10,11 +10,13 @@
};
$(function() {
- hideEndFade($('.scrolling-tabs'));
+ var $scrollingTabs = $('.scrolling-tabs');
+
+ hideEndFade($scrollingTabs);
$(window).off('resize.nav').on('resize.nav', function() {
- return hideEndFade($('.scrolling-tabs'));
+ return hideEndFade($scrollingTabs);
});
- return $('.scrolling-tabs').on('scroll', function(event) {
+ $scrollingTabs.off('scroll').on('scroll', function(event) {
var $this, currentPosition, maxPosition;
$this = $(this);
currentPosition = $this.scrollLeft();
@@ -22,6 +24,23 @@
$this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
return $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
});
+
+ $scrollingTabs.each(function () {
+ var $this = $(this),
+ scrollingTabWidth = $this.width(),
+ $active = $this.find('.active'),
+ activeWidth = $active.width();
+
+ if ($active.length) {
+ var offset = $active.offset().left + activeWidth;
+
+ if (offset > scrollingTabWidth - 30) {
+ var scrollLeft = scrollingTabWidth / 2;
+ scrollLeft = (offset - scrollLeft) - (activeWidth / 2);
+ $this.scrollLeft(scrollLeft);
+ }
+ }
+ });
});
}).call(this);
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
index 8d5e52286b7..d9b07c10a49 100644
--- a/app/assets/javascripts/lib/chart.js
+++ b/app/assets/javascripts/lib/chart.js
@@ -3,5 +3,4 @@
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js
index 8ee81804513..a88e640f298 100644
--- a/app/assets/javascripts/lib/cropper.js
+++ b/app/assets/javascripts/lib/cropper.js
@@ -3,5 +3,4 @@
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
index 31e6033e756..ee1baf54803 100644
--- a/app/assets/javascripts/lib/d3.js
+++ b/app/assets/javascripts/lib/d3.js
@@ -3,5 +3,4 @@
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js
index 923c575dcfe..6df427bc2b1 100644
--- a/app/assets/javascripts/lib/raphael.js
+++ b/app/assets/javascripts/lib/raphael.js
@@ -1,13 +1,8 @@
/*= require raphael */
-
-
/*= require g.raphael */
-
-
/*= require g.bar */
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index d4d5927d3b0..8fdf4646cd8 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -29,6 +29,7 @@
if (setTimeago) {
$timeagoEls.timeago();
$timeagoEls.tooltip('destroy');
+ // Recreate with custom template
return $timeagoEls.tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb
deleted file mode 100644
index 80f9936b9c2..00000000000
--- a/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-gl.emojiAliases = ->
- JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb
new file mode 100644
index 00000000000..aeb86c9fa5b
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb
@@ -0,0 +1,6 @@
+(function() {
+ gl.emojiAliases = function() {
+ return JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>');
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index 42b6ac0589e..5b338b00d76 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -6,6 +6,7 @@
notification = new Notification(message, opts);
setTimeout(function() {
return notification.close();
+ // Hide the notification after X amount of seconds
}, 8000);
if (onclick) {
return notification.onclick = onclick;
@@ -22,12 +23,16 @@
body: body,
icon: icon
};
+ // Let's check if the browser supports notifications
if (!('Notification' in window)) {
+ // do nothing
} else if (Notification.permission === 'granted') {
+ // If it's okay let's create a notification
return notificationGranted(message, opts, onclick);
} else if (Notification.permission !== 'denied') {
return Notification.requestPermission(function(permission) {
+ // If the user accepts, let's create a notification
if (permission === 'granted') {
return notificationGranted(message, opts, onclick);
}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index b6636de5767..d761a844be9 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -29,6 +29,7 @@
lineBefore = this.lineBefore(text, textArea);
lineAfter = this.lineAfter(text, textArea);
if (lineBefore === blockTag && lineAfter === blockTag) {
+ // To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
@@ -63,11 +64,11 @@
if (!inserted) {
try {
document.execCommand("ms-beginUndoUnit");
- } catch (undefined) {}
+ } catch (error) {}
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
try {
document.execCommand("ms-endUndoUnit");
- } catch (undefined) {}
+ } catch (error) {}
}
return this.moveCursor(textArea, tag, wrap);
};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 533310cc87c..b8d52becb3f 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -7,6 +7,8 @@
if ((base = w.gl).utils == null) {
base.utils = {};
}
+ // Returns an array containing the value(s) of the
+ // of the key passed as an argument
w.gl.utils.getParameterValues = function(sParam) {
var i, sPageURL, sParameterName, sURLVariables, values;
sPageURL = decodeURIComponent(window.location.search.substring(1));
@@ -17,12 +19,14 @@
while (i < sURLVariables.length) {
sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) {
- values.push(sParameterName[1]);
+ values.push(sParameterName[1].replace(/\+/g, ' '));
}
i++;
}
return values;
};
+ // @param {Object} params - url keys and value to merge
+ // @param {String} url
w.gl.utils.mergeUrlParams = function(params, url) {
var lastChar, newUrl, paramName, paramValue, pattern;
newUrl = decodeURIComponent(url);
@@ -37,12 +41,14 @@
newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
}
}
+ // Remove a trailing ampersand
lastChar = newUrl[newUrl.length - 1];
if (lastChar === '&') {
newUrl = newUrl.slice(0, -1);
}
return newUrl;
};
+ // removes parameter query string from url. returns the modified url
w.gl.utils.removeParamQueryString = function(url, param) {
var urlVariables, variables;
url = decodeURIComponent(url);
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index f145bd3ad74..93daea1dce7 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,17 +1,49 @@
-
+// LineHighlighter
+//
+// Handles single- and multi-line selection and highlight for blob views.
+//
/*= require jquery.scrollTo */
+//
+// ### Example Markup
+//
+// <div id="blob-content-holder">
+// <div class="file-content">
+// <div class="line-numbers">
+// <a href="#L1" id="L1" data-line-number="1">1</a>
+// <a href="#L2" id="L2" data-line-number="2">2</a>
+// <a href="#L3" id="L3" data-line-number="3">3</a>
+// <a href="#L4" id="L4" data-line-number="4">4</a>
+// <a href="#L5" id="L5" data-line-number="5">5</a>
+// </div>
+// <pre class="code highlight">
+// <code>
+// <span id="LC1" class="line">...</span>
+// <span id="LC2" class="line">...</span>
+// <span id="LC3" class="line">...</span>
+// <span id="LC4" class="line">...</span>
+// <span id="LC5" class="line">...</span>
+// </code>
+// </pre>
+// </div>
+// </div>
+//
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.LineHighlighter = (function() {
+ // CSS class applied to highlighted lines
LineHighlighter.prototype.highlightClass = 'hll';
+ // Internal copy of location.hash so we're not dependent on `location` in tests
LineHighlighter.prototype._hash = '';
function LineHighlighter(hash) {
var range;
if (hash == null) {
+ // Initialize a LineHighlighter object
+ //
+ // hash - String URL hash for dependency injection in tests
hash = location.hash;
}
this.setHash = bind(this.setHash, this);
@@ -24,6 +56,8 @@
if (range[0]) {
this.highlightRange(range);
$.scrollTo("#L" + range[0], {
+ // Scroll to the first highlighted line on initial load
+ // Offset -50 for the sticky top bar, and another -100 for some context
offset: -150
});
}
@@ -32,6 +66,12 @@
LineHighlighter.prototype.bindEvents = function() {
$('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.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.
+ //
+ // If not done this way, the line number anchor will sometimes keep its
+ // active state even when the event is cancelled, resulting in an ugly border
+ // around the link and/or a persisted underline text decoration.
return $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) {
return event.preventDefault();
});
@@ -44,6 +84,8 @@
lineNumber = $(event.target).closest('a').data('line-number');
current = this.hashToRange(this._hash);
if (!(current[0] && event.shiftKey)) {
+ // If there's no current selection, or there is but Shift wasn't held,
+ // treat this like a single-line selection.
this.setHash(lineNumber);
return this.highlightLine(lineNumber);
} else if (event.shiftKey) {
@@ -59,10 +101,23 @@
LineHighlighter.prototype.clearHighlight = function() {
return $("." + this.highlightClass).removeClass(this.highlightClass);
+ // Unhighlight previously highlighted lines
};
+ // Convert a URL hash String into line numbers
+ //
+ // hash - Hash String
+ //
+ // Examples:
+ //
+ // hashToRange('#L5') # => [5, null]
+ // hashToRange('#L5-15') # => [5, 15]
+ // hashToRange('#foo') # => [null, null]
+ //
+ // Returns an Array
LineHighlighter.prototype.hashToRange = function(hash) {
var first, last, matches;
+ //?L(\d+)(?:-(\d+))?$/)
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
if (matches && matches.length) {
first = parseInt(matches[1]);
@@ -73,10 +128,16 @@
}
};
+ // Highlight a single line
+ //
+ // lineNumber - Line number to highlight
LineHighlighter.prototype.highlightLine = function(lineNumber) {
return $("#LC" + lineNumber).addClass(this.highlightClass);
};
+ // Highlight all lines within a range
+ //
+ // range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) {
var i, lineNumber, ref, ref1, results;
if (range[1]) {
@@ -90,6 +151,7 @@
}
};
+ // Set the URL hash string
LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
var hash;
if (lastLineNumber) {
@@ -101,10 +163,15 @@
return this.__setLocationHash__(hash);
};
+ // Make the actual hash change in the browser
+ //
+ // This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) {
return history.pushState({
turbolinks: false,
url: value
+ // We're using pushState instead of assigning location.hash directly to
+ // prevent the page from scrolling on the hashchange event
}, document.title, value);
};
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 56ebf84c4f6..05644b3d03c 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,10 +1,6 @@
/*= require jquery.waitforimages */
-
-
/*= require task_list */
-
-
/*= require merge_request_tabs */
(function() {
@@ -12,6 +8,11 @@
this.MergeRequest = (function() {
function MergeRequest(opts) {
+ // Initialize MergeRequest behavior
+ //
+ // Options:
+ // action - String, current controller action
+ //
this.opts = opts != null ? opts : {};
this.submitNoteForm = bind(this.submitNoteForm, this);
this.$el = $('.merge-request');
@@ -21,6 +22,7 @@
};
})(this));
this.initTabs();
+ // Prevent duplicate event bindings
this.disableTaskList();
this.initMRBtnListeners();
if ($("a.btn-close").length) {
@@ -28,14 +30,17 @@
}
}
+ // Local jQuery finder
MergeRequest.prototype.$ = function(selector) {
return this.$el.find(selector);
};
MergeRequest.prototype.initTabs = function() {
if (this.opts.action !== 'new') {
+ // `MergeRequests#new` has no tab-persisting or lazy-loading behavior
window.mrTabs = new MergeRequestTabs(this.opts);
} else {
+ // Show the first tab (Commits)
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
}
};
@@ -96,6 +101,8 @@
url: $('form.js-issuable-update').attr('action'),
data: patchData
});
+ // TODO (rspeicher): Make the merge request description inline-editable like a
+ // note so that we can re-use its form here
};
return MergeRequest;
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index ad08209d61e..18bbfa7a459 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,6 +1,49 @@
-
+// MergeRequestTabs
+//
+// Handles persisting and restoring the current tab selection and lazily-loading
+// content on the MergeRequests#show page.
+//
/*= require jquery.cookie */
+//
+// ### Example Markup
+//
+// <ul class="nav-links merge-request-tabs">
+// <li class="notes-tab active">
+// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
+// Discussion
+// </a>
+// </li>
+// <li class="commits-tab">
+// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
+// Commits
+// </a>
+// </li>
+// <li class="diffs-tab">
+// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
+// Diffs
+// </a>
+// </li>
+// </ul>
+//
+// <div class="tab-content">
+// <div class="notes tab-pane active" id="notes">
+// Notes Content
+// </div>
+// <div class="commits tab-pane" id="commits">
+// Commits Content
+// </div>
+// <div class="diffs tab-pane" id="diffs">
+// Diffs Content
+// </div>
+// </div>
+//
+// <div class="mr-loading-status">
+// <div class="loading">
+// Loading Animation
+// </div>
+// </div>
+//
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -19,6 +62,7 @@
this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this);
+ // Store the `location` object, allowing for easier stubbing in tests
this._location = location;
this.bindEvents();
this.activateTab(this.opts.action);
@@ -77,6 +121,7 @@
}
};
+ // Activate a tab based on the current action
MergeRequestTabs.prototype.activateTab = function(action) {
if (action === 'show') {
action = 'notes';
@@ -84,20 +129,48 @@
return $(".merge-request-tabs a[data-action='" + action + "']").tab('show');
};
+ // Replaces the current Merge Request-specific action in the URL with a new one
+ //
+ // If the action is "notes", the URL is reset to the standard
+ // `MergeRequests#show` route.
+ //
+ // Examples:
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ // setCurrentAction('diffs')
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('notes')
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('commits')
+ // location.pathname # => "/namespace/project/merge_requests/1/commits"
+ //
+ // Returns the new URL String
MergeRequestTabs.prototype.setCurrentAction = function(action) {
var new_state;
+ // Normalize action, just to be safe
if (action === 'show') {
action = 'notes';
}
this.currentAction = action;
+ // Remove a trailing '/commits' or '/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
+ // Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') {
new_state += "/" + action;
}
+ // Ensure parameters and hash come along for the ride
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
url: new_state
+ // Replace the current history state with the new one without breaking
+ // Turbolinks' history.
+ //
+ // See https://github.com/rails/turbolinks/issues/363
}, document.title, new_state);
return new_state;
};
@@ -159,10 +232,10 @@
$('.hll').removeClass('hll');
locationHash = window.location.hash;
if (locationHash !== '') {
- hashClassString = "." + (locationHash.replace('#', ''));
+ dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]';
$diffLine = $(locationHash + ":not(.match)", $('#diffs'));
if (!$diffLine.is('tr')) {
- $diffLine = $('#diffs').find("td" + locationHash + ", td" + hashClassString);
+ $diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString);
} else {
$diffLine = $diffLine.find('td');
}
@@ -206,6 +279,9 @@
});
};
+ // Show or hide the loading spinner
+ //
+ // status - Boolean, true to show, false to hide
MergeRequestTabs.prototype.toggleLoading = function(status) {
return $('.mr-loading-status .loading').toggle(status);
};
@@ -232,6 +308,7 @@
MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
+ // Returns diff view type
};
MergeRequestTabs.prototype.expandViewContainer = function() {
@@ -245,6 +322,8 @@
if ($gutterIcon.is('.fa-angle-double-right')) {
return $gutterIcon.closest('a').trigger('click', [true]);
}
+ // Wait until listeners are set
+ // Only when sidebar is expanded
}, 0);
};
@@ -259,6 +338,9 @@
return $gutterIcon.closest('a').trigger('click', [true]);
}
}, 0);
+ // Expand the issuable sidebar unless the user explicitly collapsed it
+ // Wait until listeners are set
+ // Only when sidebar is collapsed
};
return MergeRequestTabs;
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index bd35b6f679d..7bbcdf59838 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -3,6 +3,12 @@
this.MergeRequestWidget = (function() {
function MergeRequestWidget(opts) {
+ // Initialize MergeRequestWidget behavior
+ //
+ // check_enable - Boolean, whether to check automerge status
+ // merge_check_url - String, URL to use to check automerge status
+ // ci_status_url - String, URL to use to check CI status
+ //
this.opts = opts;
$('#modal_merge_info').modal({
show: false
@@ -118,6 +124,8 @@
if (data.coverage) {
_this.showCICoverage(data.coverage);
}
+ // The first check should only update the UI, a notification
+ // should only be displayed on status changes
if (showNotification && !_this.firstCICheck) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index e8d51da7d58..bc1a99057d9 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -110,6 +110,7 @@
},
update: function(event, ui) {
var data;
+ // Prevents sorting from container which element has been removed.
if ($(this).find(ui.item).length > 0) {
data = $(this).sortable("serialize");
return Milestone.sortIssues(data);
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 260db626c96..3d6a4732546 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -93,6 +93,7 @@
},
hidden: function() {
$selectbox.hide();
+ // display:block overrides the hide-collapse rule
return $value.css('display', '');
},
clicked: function(selected, $el, e) {
diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch-graph.js
index c0fec1f8607..91132af273a 100644
--- a/app/assets/javascripts/network/branch-graph.js
+++ b/app/assets/javascripts/network/branch-graph.js
@@ -90,6 +90,7 @@
results = [];
while (k < this.mspace) {
this.colors.push(Raphael.getColor(.8));
+ // Skipping a few colors in the spectrum to get more contrast between colors
Raphael.getColor();
Raphael.getColor();
results.push(k++);
@@ -112,6 +113,7 @@
for (mm = j = 0, len = ref.length; j < len; mm = ++j) {
day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
+ // Dates
r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({
font: "12px Monaco, monospace",
fill: "#BBB"
@@ -119,6 +121,7 @@
cuday = day[0];
}
if (cumonth !== day[1]) {
+ // Months
r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({
font: "12px Monaco, monospace",
fill: "#EEE"
@@ -207,6 +210,7 @@
}
r = this.r;
shortrefs = commit.refs;
+ // Truncate if longer than 15 chars
if (shortrefs.length > 17) {
shortrefs = shortrefs.substr(0, 15) + "…";
}
@@ -217,6 +221,7 @@
title: commit.refs
});
textbox = text.getBBox();
+ // Create rectangle based on the size of the textbox
rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({
fill: "#000",
"fill-opacity": .5,
@@ -229,6 +234,7 @@
});
label = r.set(rect, text);
label.transform(["t", -rect.getBBox().width - 15, 0]);
+ // Set text to front
return text.toFront();
};
@@ -283,11 +289,13 @@
parentY = this.offsetY + this.unitTime * parentCommit.time;
parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space);
parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]);
+ // Set line color
if (parentCommit.space <= commit.space) {
color = this.colors[commit.space];
} else {
color = this.colors[parentCommit.space];
}
+ // Build line shape
if (parent[1] === commit.space) {
offset = [0, 5];
arrow = "l-2,5,4,0,-2,-5,0,5";
@@ -298,13 +306,17 @@
offset = [-3, 3];
arrow = "l-5,0,2,4,3,-4,-4,2";
}
+ // Start point
route = ["M", x + offset[0], y + offset[1]];
+ // Add arrow if not first parent
if (i > 0) {
route.push(arrow);
}
+ // Circumvent if overlap
if (commit.space !== parentCommit.space || commit.space !== parent[1]) {
route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5);
}
+ // End point
route.push("L", parentX1, parentY);
results.push(r.path(route).attr({
stroke: color,
@@ -325,6 +337,7 @@
"fill-opacity": .5,
stroke: "none"
});
+ // Displayed in the center
return this.element.scrollTop(y - this.graphHeight / 2);
}
};
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index 6a7422a7755..67c3e645364 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,4 +1,9 @@
-
+// This is a manifest file that'll be compiled into including all the files listed below.
+// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+// be included in the compiled file accessible from http://example.com/assets/application.js
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// the compiled file.
+//
/*= require_tree . */
(function() {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index d0d5cad813a..866a04d3e21 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,22 +1,10 @@
/*= require autosave */
-
-
/*= require autosize */
-
-
/*= require dropzone */
-
-
/*= require dropzone_input */
-
-
/*= require gfm_auto_complete */
-
-
/*= require jquery.atwho */
-
-
/*= require task_list */
(function() {
@@ -60,26 +48,43 @@
}
Notes.prototype.addBinding = function() {
+ // add note to UI after creation
$(document).on("ajax:success", ".js-main-target-form", this.addNote);
$(document).on("ajax:success", ".js-discussion-note-form", this.addDiscussionNote);
+ // catch note ajax errors
$(document).on("ajax:error", ".js-main-target-form", this.addNoteError);
+ // change note in UI after update
$(document).on("ajax:success", "form.edit-note", this.updateNote);
+ // Edit note link
$(document).on("click", ".js-note-edit", this.showEditForm);
$(document).on("click", ".note-edit-cancel", this.cancelEdit);
+ // Reopen and close actions for Issue/MR combined with note form submit
$(document).on("click", ".js-comment-button", this.updateCloseButton);
$(document).on("keyup input", ".js-note-text", this.updateTargetButtons);
+ // resolve a discussion
$(document).on('click', '.js-comment-resolve-button', this.resolveDiscussion);
+ // remove a note (in general)
$(document).on("click", ".js-note-delete", this.removeNote);
+ // delete note attachment
$(document).on("click", ".js-note-attachment-delete", this.removeAttachment);
+ // reset main target form after submit
$(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton);
$(document).on("ajax:success", ".js-main-target-form", this.resetMainTargetForm);
+ // reset main target form when clicking discard
$(document).on("click", ".js-note-discard", this.resetMainTargetForm);
+ // update the file name when an attachment is selected
$(document).on("change", ".js-note-attachment-input", this.updateFormAttachment);
+ // reply to diff/discussion notes
$(document).on("click", ".js-discussion-reply-button", this.replyToDiscussionNote);
+ // add diff note
$(document).on("click", ".js-add-diff-note-button", this.addDiffNote);
+ // hide diff note form
$(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
+ // fetch notes when tab becomes visible
$(document).on("visibilitychange", this.visibilityChange);
+ // when issue status changes, we need to refresh data
$(document).on("issuable:change", this.refresh);
+ // when a key is clicked on the notes
return $(document).on("keydown", ".js-note-text", this.keydownNoteText);
};
@@ -112,6 +117,7 @@
return;
}
$textarea = $(e.target);
+ // Edit previous note when UP arrow is hit
switch (e.which) {
case 38:
if ($textarea.val() !== '') {
@@ -123,6 +129,7 @@
return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
}
break;
+ // Cancel creating diff note or editing any note when ESCAPE is hit
case 27:
discussionNoteForm = $textarea.closest('.js-discussion-note-form');
if (discussionNoteForm.length) {
@@ -247,10 +254,13 @@
votesBlock = $('.js-awards-block').eq(0);
gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.name);
return gl.awardsHandler.scrollToAwards();
+ // render note if it not present in loaded list
+ // or skip if rendered
} else if (this.isNewNote(note)) {
this.note_ids.push(note.id);
$notesList = $('ul.main-notes-list');
$notesList.append(note.html).syntaxHighlight();
+ // Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
this.initTaskList();
this.refresh();
@@ -291,19 +301,26 @@
row = form.closest("tr");
note_html = $(note.html);
note_html.syntaxHighlight();
+ // is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']");
}
if (discussionContainer.length === 0) {
+ // insert the note and the reply button after the temp row
row.after(note.diff_discussion_html);
+ // remove the note (will be added again below)
row.next().find(".note").remove();
+ // Before that, the container didn't exist
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
+ // Add note to 'Changes' page discussions
discussionContainer.append(note_html);
+ // Init discussion on 'Discussion' page if it is merge request page
if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) {
$('ul.main-notes-list').append(note.discussion_html).syntaxHighlight();
}
} else {
+ // append new note to all matching discussions
discussionContainer.append(note_html);
}
@@ -327,11 +344,18 @@
Notes.prototype.resetMainTargetForm = function(e) {
var form;
form = $(".js-main-target-form");
+ // remove validation errors
form.find(".js-errors").remove();
+ // reset text and preview
form.find(".js-md-write-button").click();
form.find(".js-note-text").val("").trigger("input");
form.find(".js-note-text").data("autosave").reset();
- return this.updateTargetButtons(e);
+
+ var event = document.createEvent('Event');
+ event.initEvent('autosize:update', true, false);
+ form.find('.js-autosize')[0].dispatchEvent(event);
+
+ this.updateTargetButtons(e);
};
Notes.prototype.reenableTargetFormSubmitButton = function() {
@@ -349,9 +373,13 @@
Notes.prototype.setupMainTargetNoteForm = function() {
var form;
+ // find the form
form = $(".js-new-note-form");
+ // Set a global clone of the form for later cloning
this.formClone = form.clone();
+ // show the form
this.setupNoteForm(form);
+ // fix classes
form.removeClass("js-new-note-form");
form.addClass("js-main-target-form");
form.find("#note_line_code").remove();
@@ -404,18 +432,17 @@
var $form = $(xhr.target);
if ($form.attr('data-resolve-all') != null) {
- var namespacePath = $form.attr('data-namespace-path'),
- projectPath = $form.attr('data-project-path')
- discussionId = $form.attr('data-discussion-id'),
- mergeRequestId = $form.attr('data-noteable-iid'),
- namespace = namespacePath + '/' + projectPath;
+ var projectPath = $form.data('project-path')
+ discussionId = $form.data('discussion-id'),
+ mergeRequestId = $form.data('noteable-iid');
if (ResolveService != null) {
- ResolveService.toggleResolveForDiscussion(namespace, mergeRequestId, discussionId);
+ ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
}
}
this.renderDiscussionNote(note);
+ // cleanup after successfully creating a diff/discussion note
this.removeDiscussionNoteForm($form);
};
@@ -428,10 +455,12 @@
Notes.prototype.updateNote = function(_xhr, note, _status) {
var $html, $note_li;
+ // Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html);
gl.utils.localTimeAgo($('.js-timeago', $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-row-' + note.id);
$note_li.replaceWith($html);
@@ -456,15 +485,20 @@
note.addClass("is-editting");
form = note.find(".note-edit-form");
form.addClass('current-note-edit-form');
+ // Show the attachment delete link
note.find(".js-note-attachment-delete").show();
done = function($noteText) {
var noteTextVal;
+ // Neat little trick to put the cursor at the end
noteTextVal = $noteText.val();
+ // Store the original note text in a data attribute to retrieve if a user cancels edit.
form.find('form.edit-note').data('original-note', noteTextVal);
return $noteText.val('').val(noteTextVal);
};
new GLForm(form);
if ((scrollTo != null) && (myLastNote != null)) {
+ // scroll to the bottom
+ // so the open of the last element doesn't make a jump
$('html, body').scrollTop($(document).height());
return $('html, body').animate({
scrollTop: myLastNote.offset().top - 150
@@ -500,6 +534,7 @@
form = note.find(".current-note-edit-form");
note.removeClass("is-editting");
form.removeClass("current-note-edit-form");
+ // Replace markdown textarea text with original note text.
return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note'));
};
@@ -515,6 +550,9 @@
var noteId;
noteId = $(e.currentTarget).closest(".note").attr("id");
$(".note[id='" + noteId + "']").each((function(_this) {
+ // A same note appears in the "Discussion" and in the "Changes" tab, we have
+ // to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
+ // where $("#noteId") would return only one.
return function(i, el) {
var note, notes;
note = $(el);
@@ -528,13 +566,17 @@
}
}
+ // check if this is the last note for this line
if (notes.find(".note").length === 1) {
+ // "Discussions" tab
notes.closest(".timeline-entry").remove();
+ // "Changes" tab / commit view
notes.closest("tr").remove();
}
return note.remove();
};
})(this));
+ // Decrement the "Discussions" counter only once
return this.updateNotesCount(-1);
};
@@ -566,10 +608,12 @@
var form, replyLink;
form = this.formClone.clone();
replyLink = $(e.target).closest(".js-discussion-reply-button");
+ // insert the form after the button
replyLink
.closest('.discussion-reply-holder')
.hide()
.after(form);
+ // show the form
return this.setupDiscussionNoteForm(replyLink, form);
};
@@ -584,6 +628,7 @@
*/
Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) {
+ // setup note target
form.attr('id', "new-discussion-note-form-" + (dataHolder.data("discussionId")));
form.attr("data-line-code", dataHolder.data("lineCode"));
form.find("#note_type").val(dataHolder.data("noteType"));
@@ -631,6 +676,7 @@
addForm = false;
notesContentSelector = ".notes_content";
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
+ // In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineType = $link.data("lineType");
notesContentSelector += "." + lineType;
@@ -647,6 +693,7 @@
e.target = replyButton[0];
$.proxy(this.replyToDiscussionNote, replyButton[0], e).call();
} else {
+ // In parallel view, the form may not be present in one of the panes
noteForm = notesContent.find(".js-discussion-note-form");
if (noteForm.length === 0) {
addForm = true;
@@ -654,6 +701,7 @@
}
}
} else {
+ // add a notes row and insert the form
row.after(rowCssToAdd);
nextRow = row.next();
notesContent = nextRow.find(notesContentSelector);
@@ -662,6 +710,7 @@
if (addForm) {
newForm = this.formClone.clone();
newForm.appendTo(notesContent);
+ // show the form
return this.setupDiscussionNoteForm($link, newForm);
}
};
@@ -680,12 +729,15 @@
glForm = form.data('gl-form');
glForm.destroy();
form.find(".js-note-text").data("autosave").reset();
+ // show the reply button (will only work for replies)
form
.prev('.discussion-reply-holder')
.show();
if (row.is(".js-temp-notes-holder")) {
+ // remove temporary row for diff lines
return row.remove();
} else {
+ // only remove the form
return form.remove();
}
};
@@ -707,6 +759,7 @@
Notes.prototype.updateFormAttachment = function() {
var filename, form;
form = $(this).closest("form");
+ // get only the basename
filename = $(this).val().replace(/^.*[\\\/]/, "");
return form.find(".js-attachment-filename").text(filename);
};
@@ -799,7 +852,6 @@
.closest('form')
.attr('data-discussion-id', discussionId)
.attr('data-resolve-all', 'true')
- .attr('data-namespace-path', $this.attr('data-namespace-path'))
.attr('data-project-path', $this.attr('data-project-path'));
};
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 5fd75799640..5200487814f 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,9 +1,15 @@
+// MarkdownPreview
+//
+// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
+// and showing a warning when more than `x` users are referenced.
+//
(function() {
var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector;
this.MarkdownPreview = (function() {
function MarkdownPreview() {}
+ // Minimum number of users referenced before triggering a warning
MarkdownPreview.prototype.referenceThreshold = 10;
MarkdownPreview.prototype.ajaxCache = {};
@@ -101,8 +107,10 @@
return;
}
lastTextareaPreviewed = $form.find('textarea.markdown-area');
+ // 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();
return markdownPreview.showPreview($form);
@@ -113,8 +121,10 @@
return;
}
lastTextareaPreviewed = null;
+ // toggle tabs
$form.find(writeButtonSelector).parent().addClass('active');
$form.find(previewButtonSelector).parent().removeClass('active');
+ // toggle content
$form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus();
return $form.find('.md-preview-holder').hide();
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index a3eea316f67..30cd6f6e470 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -5,6 +5,7 @@
GitLabCrop = (function() {
var FILENAMEREGEX;
+ // Matches everything but the file name
FILENAMEREGEX = /^.*[\\\/]/;
function GitLabCrop(input, opts) {
@@ -17,11 +18,18 @@
this.onModalShow = bind(this.onModalShow, this);
this.onPickImageClick = bind(this.onPickImageClick, this);
this.fileInput = $(input);
+ // We should rename to avoid spec to fail
+ // Form will submit the proper input filed with a file using FormData
this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger");
+ // Set defaults
this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg;
+ // Required params
+ // Ensure needed elements are jquery objects
+ // If selector is provided we will convert them to a jQuery Object
this.filename = this.getElement(this.filename);
this.previewImage = this.getElement(this.previewImage);
this.pickImageEl = this.getElement(this.pickImageEl);
+ // Modal elements usually are outside the @form element
this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop;
this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn;
this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
@@ -93,8 +101,8 @@
return this.modalCropImg.attr('src', '').cropper('destroy');
};
- GitLabCrop.prototype.onUploadImageBtnClick = function(e) {
- e.preventDefault();
+ GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image
+ e.preventDefault(); // Destroy cropper instance
this.setBlob();
this.setPreview();
this.modalCrop.modal('hide');
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index ed1d87abafe..60f9fba5777 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -11,9 +11,11 @@
this.form = (ref = opts.form) != null ? ref : $('.edit-user');
$('.js-preferences-form').on('change.preference', 'input[type=radio]', function() {
return $(this).parents('form').submit();
+ // Automatically submit the Preferences form when any of its radio buttons change
});
$('#user_notification_email').on('change', function() {
return $(this).parents('form').submit();
+ // Automatically submit email form when it changes
});
$('.update-username').on('ajax:before', function() {
$('.loading-username').show();
@@ -76,6 +78,7 @@
},
complete: function() {
window.scrollTo(0, 0);
+ // Enable submit button after requests ends
return self.form.find(':input[disabled]').enable();
}
});
@@ -93,6 +96,7 @@
if (comment && comment.length > 1 && $title.val() === '') {
return $title.val(comment[1]).change();
}
+ // Extract the SSH Key title from its comment
});
if (gl.utils.getPagePath() === 'profiles') {
return new Profile();
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
index b95faadc8e7..d6e4d9f7ad8 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -3,5 +3,4 @@
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 66e097c0a28..a6c015299a0 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -11,7 +11,13 @@
url = $("#project_clone").val();
$('#project_clone').val(url);
return $('.clone').text(url);
+ // Git protocol switcher
+ // Remove the active class for all buttons (ssh, http, kerberos if shown)
+ // Add the active class for the clicked button
+ // Update the input field
+ // Update the command line instructions
});
+ // Ref switcher
this.initRefSwitcher();
$('.project-refs-select').on('change', function() {
return $(this).parents('form').submit();
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 4925f0519f0..8e38ccf7e44 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -7,14 +7,16 @@
function ProjectFindFile(element1, options) {
this.element = element1;
this.options = options;
- this.goToBlob = bind(this.goToBlob, this);
this.goToTree = bind(this.goToTree, this);
this.selectRowDown = bind(this.selectRowDown, this);
this.selectRowUp = bind(this.selectRowUp, this);
this.filePaths = {};
this.inputElement = this.element.find(".file-finder-input");
+ // init event
this.initEvent();
+ // focus text input box
this.inputElement.focus();
+ // load file list
this.load(this.options.url);
}
@@ -33,15 +35,6 @@
}
};
})(this));
- return this.element.find(".tree-content-holder .tree-table").on("click", function(event) {
- var path;
- if (event.target.nodeName !== "A") {
- path = this.element.find(".tree-item-file-name a", this).attr("href");
- if (path) {
- return location.href = path;
- }
- }
- });
};
ProjectFindFile.prototype.findFile = function() {
@@ -49,8 +42,10 @@
searchText = this.inputElement.val();
result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths;
return this.renderList(result, searchText);
+ // find file
};
+ // files pathes load
ProjectFindFile.prototype.load = function(url) {
return $.ajax({
url: url,
@@ -67,6 +62,7 @@
});
};
+ // render result
ProjectFindFile.prototype.renderList = function(filePaths, searchText) {
var blobItemUrl, filePath, html, i, j, len, matches, results;
this.element.find(".tree-table > tbody").empty();
@@ -86,6 +82,7 @@
return results;
};
+ // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
highlighter = function(element, text, matches) {
var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched;
lastIndex = 0;
@@ -110,13 +107,15 @@
return element.append(document.createTextNode(text.substring(lastIndex)));
};
+ // make tbody row html
ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) {
var $tr;
- $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>");
+ $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>");
if (matches) {
$tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl));
} else {
- $tr.find("a").attr("href", blobItemUrl).text(filePath);
+ $tr.find("a").attr("href", blobItemUrl);
+ $tr.find(".str-truncated").text(filePath);
}
return $tr;
};
@@ -155,14 +154,6 @@
return location.href = this.options.treeUrl;
};
- ProjectFindFile.prototype.goToBlob = function() {
- var path;
- path = this.element.find(".tree-item.selected .tree-item-file-name a").attr("href");
- if (path) {
- return location.href = path;
- }
- };
-
return ProjectFindFile;
})();
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 798f15e40a0..a787b11f2a9 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -4,6 +4,8 @@
this.ProjectNew = (function() {
function ProjectNew() {
this.toggleSettings = bind(this.toggleSettings, this);
+ this.$selects = $('.features select');
+
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
$('.project-edit-container').hide();
@@ -15,18 +17,24 @@
}
ProjectNew.prototype.toggleSettings = function() {
- this._showOrHide('#project_builds_enabled', '.builds-feature');
- return this._showOrHide('#project_merge_requests_enabled', '.merge-requests-feature');
+ var self = this;
+
+ this.$selects.each(function () {
+ var $select = $(this),
+ className = $select.data('field').replace(/_/g, '-')
+ .replace('access-level', 'feature');
+ self._showOrHide($select, '.' + className);
+ });
};
ProjectNew.prototype.toggleSettingsOnclick = function() {
- return $('#project_builds_enabled, #project_merge_requests_enabled').on('click', this.toggleSettings);
+ this.$selects.on('change', this.toggleSettings);
};
ProjectNew.prototype._showOrHide = function(checkElement, container) {
- var $container;
- $container = $(container);
- if ($(checkElement).prop('checked')) {
+ var $container = $(container);
+
+ if ($(checkElement).val() !== '0') {
return $container.show();
} else {
return $container.hide();
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
index 8ca4c427912..c8cfc9a9ba8 100644
--- a/app/assets/javascripts/project_show.js
+++ b/app/assets/javascripts/project_show.js
@@ -7,3 +7,5 @@
})();
}).call(this);
+
+// I kept class for future
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index 4f415b05dbc..04fb49552e8 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -33,6 +33,7 @@
$('.projects-list-holder').replaceWith(data.html);
return history.replaceState({
page: project_filter_url
+ // Change url so if user reload a page - search results are saved
}, document.title, project_filter_url);
},
dataType: "json"
diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branch_dropdown.js.es6
index 6738dc8862d..983322cbecc 100644
--- a/app/assets/javascripts/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branch_dropdown.js.es6
@@ -45,6 +45,7 @@ class ProtectedBranchDropdown {
}
onClickCreateWildcard() {
+ // Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex(0);
}
diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6
index 40bc4adb71b..15a6dca2875 100644
--- a/app/assets/javascripts/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branch_edit.js.es6
@@ -40,7 +40,6 @@
dataType: 'json',
data: {
_method: 'PATCH',
- id: this.$wrap.data('banchId'),
protected_branch: {
merge_access_levels_attributes: [{
id: this.$allowedToMergeDropdown.data('access-level-id'),
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 227e8c696b4..678d836f56f 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -24,6 +24,7 @@
this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this);
this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this);
this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || '';
+ // Dropdown Element
this.dropdown = this.wrap.find('.dropdown');
this.dropdownContent = this.dropdown.find('.dropdown-content');
this.locationBadgeEl = this.getElement('.location-badge');
@@ -35,6 +36,7 @@
this.repositoryInputEl = this.getElement('#repository_ref');
this.clearInput = this.getElement('.js-clear-input');
this.saveOriginalState();
+ // Only when user is logged in
if (gon.current_user_id) {
this.createAutocomplete();
}
@@ -43,6 +45,7 @@
this.bindEvents();
}
+ // Finds an element inside wrapper element
SearchAutocomplete.prototype.getElement = function(selector) {
return this.wrap.find(selector);
};
@@ -82,6 +85,7 @@
}
return;
}
+ // Prevent multiple ajax calls
if (this.loadingSuggestions) {
return;
}
@@ -92,14 +96,17 @@
term: term
}, function(response) {
var data, firstCategory, i, lastCategory, len, suggestion;
+ // Hide dropdown menu if no suggestions returns
if (!response.length) {
_this.disableAutocomplete();
return;
}
data = [];
+ // List results
firstCategory = true;
for (i = 0, len = response.length; i < len; i++) {
suggestion = response[i];
+ // Add group header before list each group
if (lastCategory !== suggestion.category) {
if (!firstCategory) {
data.push('separator');
@@ -119,6 +126,7 @@
url: suggestion.url
});
}
+ // Add option to proceed with the search
if (data.length) {
data.push('separator');
data.push({
@@ -169,11 +177,13 @@
SearchAutocomplete.prototype.serializeState = function() {
return {
+ // Search Criteria
search_project_id: this.projectInputEl.val(),
group_id: this.groupInputEl.val(),
search_code: this.searchCodeInputEl.val(),
repository_ref: this.repositoryInputEl.val(),
scope: this.scopeInputEl.val(),
+ // Location badge
_location: this.locationBadgeEl.text()
};
};
@@ -194,6 +204,7 @@
SearchAutocomplete.prototype.enableAutocomplete = function() {
var _this;
+ // No need to enable anything if user is not logged in
if (!gon.current_user_id) {
return;
}
@@ -206,18 +217,22 @@
};
SearchAutocomplete.prototype.onSearchInputKeyDown = function() {
+ // Saves last length of the entered text
return this.saveTextLength();
};
SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) {
switch (e.keyCode) {
case KEYCODE.BACKSPACE:
+ // when trying to remove the location badge
if (this.lastTextLength === 0 && this.badgePresent()) {
this.removeLocationBadge();
}
+ // When removing the last character and no badge is present
if (this.lastTextLength === 1) {
this.disableAutocomplete();
}
+ // When removing any character from existin value
if (this.lastTextLength > 1) {
this.enableAutocomplete();
}
@@ -232,9 +247,12 @@
case KEYCODE.DOWN:
return;
default:
+ // Handle the case when deleting the input value other than backspace
+ // e.g. Pressing ctrl + backspace or ctrl + x
if (this.searchInput.val() === '') {
this.disableAutocomplete();
} else {
+ // We should display the menu only when input is not empty
if (e.keyCode !== KEYCODE.ENTER) {
this.enableAutocomplete();
}
@@ -243,7 +261,9 @@
this.wrap.toggleClass('has-value', !!e.target.value);
};
+ // Avoid falsy value to be returned
SearchAutocomplete.prototype.onSearchInputClick = function(e) {
+ // Prevents closing the dropdown menu
return e.stopImmediatePropagation();
};
@@ -267,6 +287,7 @@
SearchAutocomplete.prototype.onSearchInputBlur = function(e) {
this.isFocused = false;
this.wrap.removeClass('search-active');
+ // If input is blank then restore state
if (this.searchInput.val() === '') {
return this.restoreOriginalState();
}
@@ -311,6 +332,7 @@
results = [];
for (i = 0, len = inputs.length; i < len; i++) {
input = inputs[i];
+ // _location isnt a input
if (input === '_location') {
break;
}
@@ -367,4 +389,41 @@
})();
+ $(function() {
+ var $projectOptionsDataEl = $('.js-search-project-options');
+ var $groupOptionsDataEl = $('.js-search-group-options');
+ var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
+
+ if ($projectOptionsDataEl.length) {
+ gl.projectOptions = gl.projectOptions || {};
+
+ var projectPath = $projectOptionsDataEl.data('project-path');
+
+ gl.projectOptions[projectPath] = {
+ name: $projectOptionsDataEl.data('name'),
+ issuesPath: $projectOptionsDataEl.data('issues-path'),
+ mrPath: $projectOptionsDataEl.data('mr-path')
+ };
+ }
+
+ if ($groupOptionsDataEl.length) {
+ gl.groupOptions = gl.groupOptions || {};
+
+ var groupPath = $groupOptionsDataEl.data('group-path');
+
+ gl.groupOptions[groupPath] = {
+ name: $groupOptionsDataEl.data('name'),
+ issuesPath: $groupOptionsDataEl.data('issues-path'),
+ mrPath: $groupOptionsDataEl.data('mr-path')
+ };
+ }
+
+ if ($dashboardOptionsDataEl.length) {
+ gl.dashboardOptions = {
+ issuesPath: $dashboardOptionsDataEl.data('issues-path'),
+ mrPath: $dashboardOptionsDataEl.data('mr-path')
+ };
+ }
+ });
+
}).call(this);
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 3b28332854a..3aa8536d40a 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -86,6 +86,7 @@
var defaultStopCallback;
defaultStopCallback = Mousetrap.stopCallback;
return function(e, element, combo) {
+ // allowed shortcuts if textarea, input, contenteditable are focused
if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
return false;
} else {
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 6c78914d338..92ce31969e3 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -14,8 +14,10 @@
ShortcutsFindFile.__super__.constructor.call(this);
_oldStopCallback = Mousetrap.stopCallback;
Mousetrap.stopCallback = (function(_this) {
+ // override to fire shortcuts action when focus in textbox
return function(event, element, combo) {
if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) {
+ // when press up/down key in textbox, cusor prevent to move to home/end
event.preventDefault();
return false;
}
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 3f3a8a9dfd9..235bf4f95ec 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,7 +1,5 @@
/*= require mousetrap */
-
-
/*= require shortcuts_navigation */
(function() {
@@ -43,16 +41,20 @@
if (selected.trim() === "") {
return;
}
+ // Put a '>' character before each non-empty line in the selection
quote = _.map(selected.split("\n"), function(val) {
if (val.trim() !== '') {
return "> " + val + "\n";
}
});
+ // If replyField already has some content, add a newline before our quote
separator = replyField.val().trim() !== "" && "\n" || '';
replyField.val(function(_, current) {
return current + separator + quote.join('') + "\n";
});
+ // Trigger autosave for the added text
replyField.trigger('input');
+ // Focus the input field
return replyField.focus();
}
};
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 469e25482bb..b04159420d1 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -34,6 +34,9 @@
Mousetrap.bind('g i', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues');
});
+ Mousetrap.bind('g l', function() {
+ ShortcutsNavigation.findAndFollowLink('.shortcuts-issue-boards');
+ });
Mousetrap.bind('g m', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests');
});
diff --git a/app/assets/javascripts/sidebar.js b/app/assets/javascripts/sidebar.js
deleted file mode 100644
index bd0c1194b36..00000000000
--- a/app/assets/javascripts/sidebar.js
+++ /dev/null
@@ -1,41 +0,0 @@
-(function() {
- var collapsed, expanded, toggleSidebar;
-
- collapsed = 'page-sidebar-collapsed';
-
- expanded = 'page-sidebar-expanded';
-
- toggleSidebar = function() {
- $('.page-with-sidebar').toggleClass(collapsed + " " + expanded);
- $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded");
- if ($.cookie('pin_nav') === 'true') {
- $('.navbar-fixed-top').toggleClass('header-pinned-nav');
- $('.page-with-sidebar').toggleClass('page-sidebar-pinned');
- }
- return setTimeout((function() {
- var niceScrollBars;
- niceScrollBars = $('.nav-sidebar').niceScroll();
- return niceScrollBars.updateScrollBar();
- }), 300);
- };
-
- $(document).off('click', 'body').on('click', 'body', function(e) {
- var $nav, $target, $toggle, pageExpanded;
- if ($.cookie('pin_nav') !== 'true') {
- $target = $(e.target);
- $nav = $target.closest('.sidebar-wrapper');
- pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded');
- $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle');
- if ($nav.length === 0 && pageExpanded && $toggle.length === 0) {
- $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded');
- return $('.navbar-fixed-top').toggleClass('header-collapsed header-expanded');
- }
- }
- });
-
- $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', function(e) {
- e.preventDefault();
- return toggleSidebar();
- });
-
-}).call(this);
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
new file mode 100644
index 00000000000..ead3219bc31
--- /dev/null
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -0,0 +1,95 @@
+((global) => {
+ let singleton;
+
+ const pinnedStateCookie = 'pin_nav';
+ const sidebarBreakpoint = 1024;
+
+ const pageSelector = '.page-with-sidebar';
+ const navbarSelector = '.navbar-fixed-top';
+ const sidebarWrapperSelector = '.sidebar-wrapper';
+ const sidebarContentSelector = '.nav-sidebar';
+
+ const pinnedToggleSelector = '.js-nav-pin';
+ const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle';
+
+ const pinnedPageClass = 'page-sidebar-pinned';
+ const expandedPageClass = 'page-sidebar-expanded';
+
+ const pinnedNavbarClass = 'header-sidebar-pinned';
+ const expandedNavbarClass = 'header-sidebar-expanded';
+
+ class Sidebar {
+ constructor() {
+ if (!singleton) {
+ singleton = this;
+ singleton.init();
+ }
+ return singleton;
+ }
+
+ init() {
+ this.isPinned = $.cookie(pinnedStateCookie) === 'true';
+ this.isExpanded = (
+ window.innerWidth >= sidebarBreakpoint &&
+ $(pageSelector).hasClass(expandedPageClass)
+ );
+ $(document)
+ .on('click', sidebarToggleSelector, (e) => this.toggleSidebar(e))
+ .on('click', pinnedToggleSelector, (e) => this.togglePinnedState(e))
+ .on('click', 'html, body', (e) => this.handleClickEvent(e))
+ .on('page:change', () => this.renderState());
+ this.renderState();
+ }
+
+ handleClickEvent(e) {
+ if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) {
+ const $target = $(e.target);
+ const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0;
+ const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0;
+ if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) {
+ this.toggleSidebar(e);
+ }
+ }
+ }
+
+ toggleSidebar(e) {
+ e.preventDefault();
+ this.isExpanded = !this.isExpanded;
+ this.renderState();
+ }
+
+ togglePinnedState(e) {
+ e.preventDefault();
+ this.isPinned = !this.isPinned;
+ if (!this.isPinned) {
+ this.isExpanded = false;
+ }
+ $.cookie(pinnedStateCookie, this.isPinned ? 'true' : 'false', {
+ path: gon.relative_url_root || '/',
+ expires: 3650
+ });
+ this.renderState();
+ }
+
+ renderState() {
+ $(pageSelector)
+ .toggleClass(pinnedPageClass, this.isPinned && this.isExpanded)
+ .toggleClass(expandedPageClass, this.isExpanded);
+ $(navbarSelector)
+ .toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded)
+ .toggleClass(expandedNavbarClass, this.isExpanded);
+
+ const $pinnedToggle = $(pinnedToggleSelector);
+ const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation';
+ const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide';
+ $pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
+
+ if (this.isExpanded) {
+ setTimeout(() => $(sidebarContentSelector).niceScroll().updateScrollBar(), 200);
+ }
+ }
+ }
+
+ global.Sidebar = Sidebar;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 156b9b8abec..ee6af123268 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -10,12 +10,13 @@
ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
- COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>';
+ COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
function SingleFileDiff(file) {
this.file = file;
this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file);
+ this.$toggleIcon = $('.diff-toggle-caret', this.file);
this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path');
this.isOpen = !this.diffForPath;
if (this.diffForPath) {
@@ -23,18 +24,22 @@
this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide();
this.content = null;
this.collapsedContent.after(this.loadingContent);
+ this.$toggleIcon.addClass('fa-caret-right');
} else {
this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide();
this.content.after(this.collapsedContent);
+ this.$toggleIcon.addClass('fa-caret-down');
}
- this.collapsedContent.on('click', this.toggleDiff);
- $('.file-title > a', this.file).on('click', this.toggleDiff);
+ $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
}
SingleFileDiff.prototype.toggleDiff = function(e) {
+ var $target = $(e.target);
+ if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
this.content.hide();
+ this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show();
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
@@ -42,10 +47,12 @@
} else if (this.content) {
this.collapsedContent.hide();
this.content.show();
+ this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
}
} else {
+ this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML();
}
};
diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6
new file mode 100644
index 00000000000..6f0996c0d2a
--- /dev/null
+++ b/app/assets/javascripts/snippets_list.js.es6
@@ -0,0 +1,11 @@
+(global => {
+ global.gl = global.gl || {};
+
+ gl.SnippetsList = function() {
+ var $holder = $('.snippets-list-holder');
+
+ $holder.find('.pagination').on('ajax:success', (e, data) => {
+ $holder.replaceWith(data.html);
+ });
+ }
+})(window);
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index dba62638c78..2ae7bf5fc15 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,9 +1,20 @@
+// Syntax Highlighter
+//
+// Applies a syntax highlighting color scheme CSS class to any element with the
+// `js-syntax-highlight` class
+//
+// ### Example Markup
+//
+// <div class="js-syntax-highlight"></div>
+//
(function() {
$.fn.syntaxHighlight = function() {
var $children;
if ($(this).hasClass('js-syntax-highlight')) {
+ // Given the element itself, apply highlighting
return $(this).addClass(gon.user_color_scheme);
} else {
+ // Given a parent element, recurse to any of its applicable children
$children = $(this).find('.js-syntax-highlight');
if ($children.length) {
return $children.syntaxHighlight();
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
index 23eda7d44ca..93421649ac7 100644
--- a/app/assets/javascripts/todos.js
+++ b/app/assets/javascripts/todos.js
@@ -129,16 +129,21 @@
var currPage, currPages, newPages, pageParams, url;
currPages = this.getTotalPages();
currPage = this.getCurrentPage();
+ // Refresh if no remaining Todos
if (!total) {
location.reload();
return;
}
+ // Do nothing if no pagination
if (!currPages) {
return;
}
newPages = Math.ceil(total / this.getTodosPerPage());
+ // Includes query strings
url = location.href;
+ // If new total of pages is different than we have now
if (newPages !== currPages) {
+ // Redirect to previous page if there's one available
if (currPages > 1 && currPage === currPages) {
pageParams = {
page: currPages - 1
@@ -155,6 +160,7 @@
if (!todoLink) {
return;
}
+ // Allow Meta-Click or Mouse3-click to open in a new tab
if (e.metaKey || e.which === 2) {
e.preventDefault();
return window.open(todoLink, '_blank');
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index 78e159a7ed9..9b7be17c4fe 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -2,6 +2,8 @@
this.TreeView = (function() {
function TreeView() {
this.initKeyNav();
+ // Code browser tree slider
+ // Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
$(".tree-content-holder .tree-item").on('click', function(e) {
var $clickedEl, path;
$clickedEl = $(e.target);
@@ -15,6 +17,7 @@
}
}
});
+ // Show the "Loading commit data" for only the first element
$('span.log_loading:first').removeClass('hide');
}
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index 9ba847fb0c2..ce2930c7fc7 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -1,3 +1,7 @@
+// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
+//
+// State Flow #1: setup -> in_progress -> authenticated -> POST to server
+// State Flow #2: setup -> in_progress -> error -> setup
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -15,6 +19,17 @@
this.appId = u2fParams.app_id;
this.challenge = u2fParams.challenge;
this.signRequests = u2fParams.sign_requests.map(function(request) {
+ // The U2F Javascript API v1.1 requires a single challenge, with
+ // _no challenges per-request_. The U2F Javascript API v1.0 requires a
+ // challenge per-request, which is done by copying the single challenge
+ // into every request.
+ //
+ // In either case, we don't need the per-request challenges that the server
+ // has generated, so we can remove them.
+ //
+ // Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
+ // This can be removed once we upgrade.
+ // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
return _(request).omit('challenge');
});
}
@@ -41,6 +56,7 @@
})(this), 10);
};
+ // Rendering #
U2FAuthenticate.prototype.templates = {
"notSupported": "#js-authenticate-u2f-not-supported",
"setup": '#js-authenticate-u2f-setup',
@@ -75,6 +91,8 @@
U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) {
this.renderTemplate('authenticated');
+ // Prefer to do this instead of interpolating using Underscore templates
+ // because of JSON escaping issues.
return this.container.find("#js-device-response").val(deviceResponse);
};
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index c87e0840df3..926912fa988 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,3 +1,7 @@
+// Register U2F (universal 2nd factor) devices for users to authenticate with.
+//
+// State Flow #1: setup -> in_progress -> registered -> POST to server
+// State Flow #2: setup -> in_progress -> error -> setup
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -39,6 +43,7 @@
})(this), 10);
};
+ // Rendering #
U2FRegister.prototype.templates = {
"notSupported": "#js-register-u2f-not-supported",
"setup": '#js-register-u2f-setup',
@@ -73,6 +78,8 @@
U2FRegister.prototype.renderRegistered = function(deviceResponse) {
this.renderTemplate('registered');
+ // Prefer to do this instead of interpolating using Underscore templates
+ // because of JSON escaping issues.
return this.container.find("#js-device-response").val(deviceResponse);
};
diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js
index e5e75701fee..8a657780eb6 100644
--- a/app/assets/javascripts/user_tabs.js
+++ b/app/assets/javascripts/user_tabs.js
@@ -1,3 +1,61 @@
+// UserTabs
+//
+// Handles persisting and restoring the current tab selection and lazily-loading
+// content on the Users#show page.
+//
+// ### Example Markup
+//
+// <ul class="nav-links">
+// <li class="activity-tab active">
+// <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
+// Activity
+// </a>
+// </li>
+// <li class="groups-tab">
+// <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
+// Groups
+// </a>
+// </li>
+// <li class="contributed-tab">
+// <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
+// Contributed projects
+// </a>
+// </li>
+// <li class="projects-tab">
+// <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
+// Personal projects
+// </a>
+// </li>
+// <li class="snippets-tab">
+// <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
+// </a>
+// </li>
+// </ul>
+//
+// <div class="tab-content">
+// <div class="tab-pane" id="activity">
+// Activity Content
+// </div>
+// <div class="tab-pane" id="groups">
+// Groups Content
+// </div>
+// <div class="tab-pane" id="contributed">
+// Contributed projects content
+// </div>
+// <div class="tab-pane" id="projects">
+// Projects content
+// </div>
+// <div class="tab-pane" id="snippets">
+// Snippets content
+// </div>
+// </div>
+//
+// <div class="loading-status">
+// <div class="loading">
+// Loading Animation
+// </div>
+// </div>
+//
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -6,18 +64,23 @@
this.tabShown = bind(this.tabShown, this);
var i, item, len, ref, ref1, ref2, ref3;
this.action = (ref = opts.action) != null ? ref : 'activity', this.defaultAction = (ref1 = opts.defaultAction) != null ? ref1 : 'activity', this.parentEl = (ref2 = opts.parentEl) != null ? ref2 : $(document);
+ // Make jQuery object if selector is provided
if (typeof this.parentEl === 'string') {
this.parentEl = $(this.parentEl);
}
+ // Store the `location` object, allowing for easier stubbing in tests
this._location = location;
+ // Set tab states
this.loaded = {};
ref3 = this.parentEl.find('.nav-links a');
for (i = 0, len = ref3.length; i < len; i++) {
item = ref3[i];
this.loaded[$(item).attr('data-action')] = false;
}
+ // Actions
this.actions = Object.keys(this.loaded);
this.bindEvents();
+ // Set active tab
if (this.action === 'show') {
this.action = this.defaultAction;
}
@@ -25,6 +88,7 @@
}
UserTabs.prototype.bindEvents = function() {
+ // Toggle event listeners
return this.parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]').on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', this.tabShown);
};
@@ -74,6 +138,7 @@
tabSelector = 'div#' + action;
_this.parentEl.find(tabSelector).html(data.html);
_this.loaded[action] = true;
+ // Fix tooltips
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
};
})(this)
@@ -97,13 +162,17 @@
UserTabs.prototype.setCurrentAction = function(action) {
var new_state, regExp;
+ // Remove possible actions from URL
regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$');
new_state = this._location.pathname;
+ // remove trailing slashes
new_state = new_state.replace(/\/+$/, "");
new_state = new_state.replace(regExp, '');
+ // Append the new action if we're on a tab other than 'activity'
if (action !== this.defaultAction) {
new_state += "/" + action;
}
+ // Ensure parameters and hash come along for the ride
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 74ecf4f4cf9..3bd4c3c066f 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -11,6 +11,8 @@
this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
this.months = [];
+ // Loop through the timestamps to create a group of objects
+ // The group of objects will be grouped based on the day of the week they are
this.timestampsTmp = [];
var group = 0;
@@ -27,14 +29,17 @@
date.setDate(date.getDate() + i);
var day = date.getDay();
- var count = timestamps[date.getTime() * 0.001];
+ var count = timestamps[dateFormat(date, 'yyyy-mm-dd')];
+ // Create a new group array if this is the first day of the week
+ // or if is first object
if ((day === 0 && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
group++;
}
var innerArray = this.timestampsTmp[group - 1];
+ // Push to the inner array the values that will be used to render map
innerArray.push({
count: count || 0,
date: date,
@@ -42,8 +47,10 @@
});
}
+ // Init color functions
this.colorKey = this.initColorKey();
this.color = this.initColor();
+ // Init the svg element
this.renderSvg(group);
this.renderDays();
this.renderMonths();
@@ -52,8 +59,22 @@
this.initTooltips();
}
+ // Add extra padding for the last month label if it is also the last column
+ Calendar.prototype.getExtraWidthPadding = function(group) {
+ var extraWidthPadding = 0;
+ var lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth();
+ var secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth();
+
+ if (lastColMonth != secondLastColMonth) {
+ extraWidthPadding = 3;
+ }
+
+ return extraWidthPadding;
+ }
+
Calendar.prototype.renderSvg = function(group) {
- return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', (group + 1) * this.daySizeWithSpace).attr('height', 167).attr('class', 'contrib-calendar');
+ var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
+ return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', width).attr('height', 167).attr('class', 'contrib-calendar');
};
Calendar.prototype.renderDays = function() {
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index b95faadc8e7..d6e4d9f7ad8 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -3,5 +3,4 @@
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index b76a0831cf9..235d1d34bf6 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -81,6 +81,7 @@
if (term.length === 0) {
showDivider = 0;
if (firstUser) {
+ // Move current user to the front of the list
for (index = j = 0, len = users.length; j < len; index = ++j) {
obj = users[index];
if (obj.username === firstUser) {
@@ -115,6 +116,7 @@
if (showDivider) {
users.splice(showDivider, 0, "divider");
}
+ // Send the data back
return callback(users);
});
},
@@ -140,6 +142,7 @@
inputId: 'issue_assignee_id',
hidden: function(e) {
$selectbox.hide();
+ // display:block overrides the hide-collapse rule
return $value.css('display', '');
},
clicked: function(user, $el, e) {
@@ -183,6 +186,7 @@
img = "<img src='" + avatar + "' class='avatar avatar-inline' width='30' />";
}
}
+ // split into three parts so we can remove the username section if nessesary
listWithName = "<li> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>";
listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>";
listClosingTags = "</a> </li>";
@@ -221,6 +225,7 @@
};
if (query.term.length === 0) {
if (firstUser) {
+ // Move current user to the front of the list
ref = data.results;
for (index = j = 0, len = ref.length; j < len; index = ++j) {
obj = ref[index];
@@ -277,6 +282,7 @@
return _this.formatSelection.apply(_this, args);
},
dropdownCssClass: "ajax-users-dropdown",
+ // we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
@@ -324,6 +330,8 @@
});
};
+ // Return users list. Filtered by query
+ // Only active users retrieved
UsersSelect.prototype.users = function(query, options, callback) {
var url;
url = this.buildUrl(this.usersPath);
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index 71236c6a27d..777b32b41c9 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,21 +1,34 @@
-
+// Zen Mode (full screen) textarea
+//
/*= provides zen_mode:enter */
-
-
/*= provides zen_mode:leave */
-
-
+//
/*= require jquery.scrollTo */
-
-
/*= require dropzone */
-
-
/*= require mousetrap */
-
-
/*= require mousetrap/pause */
+//
+// ### Events
+//
+// `zen_mode:enter`
+//
+// Fired when the "Edit in fullscreen" link is clicked.
+//
+// **Synchronicity** Sync
+// **Bubbles** Yes
+// **Cancelable** No
+// **Target** a.js-zen-enter
+//
+// `zen_mode:leave`
+//
+// Fired when the "Leave Fullscreen" link is clicked.
+//
+// **Synchronicity** Sync
+// **Bubbles** Yes
+// **Cancelable** No
+// **Target** a.js-zen-leave
+//
(function() {
this.ZenMode = (function() {
function ZenMode() {
@@ -40,6 +53,7 @@
};
})(this));
$(document).on('keydown', function(e) {
+ // Esc
if (e.keyCode === 27) {
e.preventDefault();
return $(document).trigger('zen_mode:leave');
@@ -52,6 +66,7 @@
this.active_backdrop = $(backdrop);
this.active_backdrop.addClass('fullscreen');
this.active_textarea = this.active_backdrop.find('textarea');
+ // Prevent a user-resized textarea from persisting to fullscreen
this.active_textarea.removeAttr('style');
return this.active_textarea.focus();
};
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 1fec61bdba1..1e9a45c19b8 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -8,65 +8,44 @@
// Copyright (c) 2016 Daniel Eden
.animated {
- -webkit-animation-duration: 1s;
- animation-duration: 1s;
- -webkit-animation-fill-mode: both;
- animation-fill-mode: both;
-}
-
-.animated.infinite {
- -webkit-animation-iteration-count: infinite;
- animation-iteration-count: infinite;
-}
+ @include webkit-prefix(animation-duration, 1s);
+ @include webkit-prefix(animation-fill-mode, both);
-.animated.hinge {
- -webkit-animation-duration: 2s;
- animation-duration: 2s;
-}
+ &.infinite {
+ @include webkit-prefix(animation-iteration-count, infinite);
+ }
-.animated.flipOutX,
-.animated.flipOutY,
-.animated.bounceIn,
-.animated.bounceOut {
- -webkit-animation-duration: .75s;
- animation-duration: .75s;
-}
+ &.once {
+ @include webkit-prefix(animation-iteration-count, 1);
+ }
-@-webkit-keyframes pulse {
- from {
- -webkit-transform: scale3d(1, 1, 1);
- transform: scale3d(1, 1, 1);
+ &.hinge {
+ @include webkit-prefix(animation-duration, 2s);
}
- 50% {
- -webkit-transform: scale3d(1.05, 1.05, 1.05);
- transform: scale3d(1.05, 1.05, 1.05);
+ &.flipOutX,
+ &.flipOutY,
+ &.bounceIn,
+ &.bounceOut {
+ @include webkit-prefix(animation-duration, .75s);
}
- to {
- -webkit-transform: scale3d(1, 1, 1);
- transform: scale3d(1, 1, 1);
+ &.short {
+ @include webkit-prefix(animation-duration, 321ms);
+ @include webkit-prefix(animation-fill-mode, none);
}
}
-@keyframes pulse {
- from {
- -webkit-transform: scale3d(1, 1, 1);
- transform: scale3d(1, 1, 1);
+@include keyframes(pulse) {
+ from, to {
+ @include webkit-prefix(transform, scale3d(1, 1, 1));
}
50% {
- -webkit-transform: scale3d(1.05, 1.05, 1.05);
- transform: scale3d(1.05, 1.05, 1.05);
- }
-
- to {
- -webkit-transform: scale3d(1, 1, 1);
- transform: scale3d(1, 1, 1);
+ @include webkit-prefix(transform, scale3d(1.05, 1.05, 1.05));
}
}
.pulse {
- -webkit-animation-name: pulse;
- animation-name: pulse;
+ @include webkit-prefix(animation-name, pulse);
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 7ce203d2ec7..d315db4cb32 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -19,10 +19,8 @@
&.diff-collapsed {
padding: 5px;
- cursor: pointer;
-
- &:hover {
- background-color: $row-hover;
+ .click-to-expand {
+ cursor: pointer;
}
}
}
@@ -129,8 +127,6 @@
position: relative;
.avatar-holder {
- margin-bottom: 16px;
-
.avatar, .identicon {
margin: 0 auto;
float: none;
@@ -143,13 +139,7 @@
.cover-title {
color: $gl-header-color;
- margin: 0;
- font-size: 24px;
- font-weight: normal;
- margin-bottom: 10px;
- color: #4c4e54;
font-size: 23px;
- line-height: 1.1;
h1 {
color: $gl-gray-dark;
@@ -213,6 +203,9 @@
}
}
}
+ &.user-cover-block {
+ padding: 24px 0 0;
+ }
.group-info {
@@ -249,6 +242,10 @@
> .controls {
float: right;
}
+
+ .new-branch {
+ margin-top: 3px;
+ }
}
.content-block-small {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index cd3ddf5fee9..ce489f7c3de 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -206,7 +206,9 @@
}
svg, .fa {
- margin-right: 3px;
+ &:not(:last-child) {
+ margin-right: 3px;
+ }
}
}
@@ -334,3 +336,9 @@
box-shadow: inset 0 0 0 white;
}
}
+
+@media (max-width: $screen-xs-max) {
+ .btn-wide-on-xs {
+ width: 100%;
+ }
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 8984bce616c..5957dce89bc 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -53,7 +53,7 @@ pre {
&.well-pre {
border: 1px solid #eee;
- background: #f9f9f9;
+ background: $gray-light;
border-radius: 0;
color: #555;
}
@@ -225,7 +225,7 @@ li.note {
.milestone {
&.milestone-closed {
- background: #f9f9f9;
+ background: $gray-light;
}
.progress {
margin-bottom: 0;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index d3e3fc50736..81520500594 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -26,6 +26,15 @@
padding: 10px $gl-padding;
word-wrap: break-word;
border-radius: 3px 3px 0 0;
+ cursor: pointer;
+
+ &:hover {
+ background-color: $dark-background-color;
+ }
+
+ .diff-toggle-caret {
+ padding-right: 6px;
+ }
&.file-title-clear {
padding-left: 0;
@@ -63,7 +72,7 @@
&.image_file {
background: #eee;
text-align: center;
-
+
img {
padding: 20px;
max-width: 80%;
@@ -94,7 +103,6 @@
&.blame {
table {
border: none;
- box-shadow: none;
margin: 0;
}
tr {
@@ -108,19 +116,10 @@
border-right: none;
}
}
- img.avatar {
- border: 0 none;
- float: none;
- margin: 0;
- padding: 0;
- }
td.blame-commit {
- background: #f9f9f9;
- min-width: 350px;
-
- .commit-author-link {
- color: #888;
- }
+ padding: 0 10px;
+ min-width: 400px;
+ background: $gray-light;
}
td.line-numbers {
float: none;
@@ -133,12 +132,6 @@
}
td.lines {
padding: 0;
- code {
- font-family: $monospace_font;
- }
- pre {
- margin: 0;
- }
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 9209347f9bc..19827943385 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -1,6 +1,10 @@
.filter-item {
margin-right: 6px;
vertical-align: top;
+
+ &.reset-filters {
+ padding: 7px;
+ }
}
@media (min-width: $screen-sm-min) {
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 0c21d0240b3..3ac1678dd05 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -3,7 +3,8 @@
margin: 0;
margin-bottom: $gl-padding;
font-size: 14px;
- z-index: 100;
+ position: relative;
+ z-index: 1;
.flash-notice {
@extend .alert;
@@ -34,6 +35,12 @@
}
}
+.content-wrapper {
+ .flash-notice .container-fluid {
+ background-color: transparent;
+ }
+}
+
@media (max-width: $screen-md-min) {
ul.notes {
.flash-container.timeline-content {
@@ -41,4 +48,3 @@
}
}
}
-
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 1036219172e..c748f856501 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -77,10 +77,6 @@ header {
}
}
- &.header-collapsed {
- padding: 0 16px;
- }
-
.side-nav-toggle {
position: absolute;
left: -10px;
@@ -116,11 +112,15 @@ header {
.header-logo {
position: absolute;
left: 50%;
- margin-left: -18px;
top: 7px;
transition-duration: .3s;
z-index: 999;
+ #logo {
+ position: relative;
+ left: -50%;
+ }
+
svg, img {
height: 36px;
}
@@ -130,8 +130,12 @@ header {
}
@media (max-width: $screen-xs-max) {
- right: 25px;
+ right: 20px;
left: auto;
+
+ #logo {
+ left: auto;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 965fcc06518..efc348214c2 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -162,6 +162,10 @@ ul.content-list {
margin-right: 0;
}
}
+
+ .no-comments {
+ opacity: .5;
+ }
}
// When dragging a list item
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 62dc2cb3fdc..1ec08cdef23 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -44,7 +44,7 @@
}
&.active {
- background: #f9f9f9;
+ background: $gray-light;
a {
font-weight: 600;
}
@@ -85,3 +85,13 @@
#{'-webkit-' + $property}: $value;
#{$property}: $value;
}
+
+@mixin keyframes($animation-name) {
+ @-webkit-keyframes #{$animation-name} {
+ @content;
+ }
+
+ @keyframes #{$animation-name} {
+ @content;
+ }
+}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 367c7d01944..76b93b23b95 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -79,10 +79,6 @@
padding-left: 15px !important;
}
- .issue-info, .merge-request-info {
- display: none;
- }
-
.nav-links, .nav-links {
li a {
font-size: 14px;
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 9efbaf54e90..ea43f4afc37 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -99,8 +99,7 @@
.top-area {
@include clearfix;
-
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid $btn-gray-hover;
.nav-text {
padding-top: 16px;
@@ -138,7 +137,7 @@
}
li a {
- padding: 16px 10px 11px;
+ padding: 16px 15px 11px;
}
/* Small devices (phones, tablets, 768px and lower) */
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 015fe3debf9..3b7de4b57bb 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,6 +1,5 @@
.page-with-sidebar {
- padding-top: $header-height;
- padding-bottom: 25px;
+ padding: $header-height 0 25px;
transition: padding $sidebar-transition-duration;
&.page-sidebar-pinned {
@@ -15,6 +14,7 @@
bottom: 0;
left: 0;
height: 100%;
+ width: 0;
overflow: hidden;
transition: width $sidebar-transition-duration;
@include box-shadow(2px 0 16px 0 $black-transparent);
@@ -128,10 +128,8 @@
.fa {
transition: transform .15s;
- }
- &.is-active {
- .fa {
+ .page-sidebar-pinned & {
transform: rotate(90deg);
}
}
@@ -152,14 +150,6 @@
}
}
-.page-sidebar-collapsed {
- padding-left: 0;
-
- .sidebar-wrapper {
- width: 0;
- }
-}
-
.page-sidebar-expanded {
.sidebar-wrapper {
width: $sidebar_width;
@@ -175,7 +165,7 @@
}
}
-header.header-pinned-nav {
+header.header-sidebar-pinned {
@media (min-width: $sidebar-breakpoint) {
padding-left: ($sidebar_width + $gl-padding);
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 371c1bf17e1..915aa631ef8 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -125,7 +125,7 @@ $panel-inner-border: $border-color;
//
//##
-$well-bg: #f9f9f9;
+$well-bg: $gray-light;
$well-border: #eee;
//== Code
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 06874a993fa..9f2d53d5206 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -159,25 +159,18 @@
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;
+ left: -16px;
+ position: absolute;
+ text-decoration: none;
+
+ &:after {
+ content: image-url('icon_anchor.svg');
+ visibility: hidden;
+ }
}
- &:hover > a.anchor {
- $size: 14px;
- position: absolute;
- right: 100%;
- top: 50%;
- margin-top: -11px;
- margin-right: 0;
- padding-right: 15px;
- display: inline-block;
- width: $size;
- height: $size;
- background-image: image-url("icon-link.png");
- background-size: contain;
- background-repeat: no-repeat;
+ &:hover > a.anchor:after {
+ visibility: visible;
}
}
}
@@ -211,7 +204,7 @@ body {
}
h1, h2, h3, h4, h5, h6 {
- color: $gl-header-color;
+ color: $gl-title-color;
font-weight: 600;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 5da390118c6..14ec310de2d 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -10,12 +10,78 @@ $sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px;
/*
+ * Color schema
+ */
+$white-light: #fff;
+$white-normal: #ededed;
+$white-dark: #ececec;
+
+$gray-light: #fafafa;
+$gray-normal: #f5f5f5;
+$gray-dark: #ededed;
+$gray-darkest: #c9c9c9;
+
+$green-light: #38ae67;
+$green-normal: #2faa60;
+$green-dark: #2ca05b;
+
+$blue-light: #2ea8e5;
+$blue-normal: #2d9fd8;
+$blue-dark: #2897ce;
+
+$blue-medium-light: #3498cb;
+$blue-medium: #2f8ebf;
+$blue-medium-dark: #2d86b4;
+
+$orange-light: #fc8a51;
+$orange-normal: #e75e40;
+$orange-dark: #ce5237;
+
+$red-light: #e52c5a;
+$red-normal: #d22852;
+$red-dark: darken($red-normal, 5%);
+
+$black: #000;
+$black-transparent: rgba(0, 0, 0, 0.3);
+
+$border-white-light: #f1f2f4;
+$border-white-normal: #d6dae2;
+$border-white-dark: #c6cacf;
+
+$border-gray-light: #dcdcdc;
+$border-gray-normal: #d7d7d7;
+$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: #fc6d26;
+$border-orange-normal: #ce5237;
+$border-orange-dark: #c14e35;
+
+$border-red-light: #d22852;
+$border-red-normal: #ca264f;
+$border-red-dark: darken($border-red-normal, 5%);
+
+$help-well-bg: $gray-light;
+$help-well-border: #e5e5e5;
+
+$warning-message-bg: #fbf2d9;
+$warning-message-color: #9e8e60;
+$warning-message-border: #f0e2bb;
+
+/*
* UI elements
*/
$border-color: #e5e5e5;
$focus-border-color: #3aabf0;
$table-border-color: #f0f0f0;
-$background-color: #fafafa;
+$background-color: $gray-light;
$dark-background-color: #f5f5f5;
$table-text-gray: #8f8f8f;
@@ -35,7 +101,8 @@ $gl-icon-color: $gl-placeholder-color;
$gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
-$gl-header-color: $gl-title-color;
+$gl-gray-light: $gl-placeholder-color;
+$gl-header-color: #4c4e54;
/*
* Lists
@@ -90,73 +157,6 @@ $btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
-/*
- * Color schema
- */
-
-$white-light: #fff;
-$white-normal: #ededed;
-$white-dark: #ececec;
-
-$gray-light: #faf9f9;
-$gray-normal: #f5f5f5;
-$gray-dark: #ededed;
-$gray-darkest: #c9c9c9;
-
-$green-light: #38ae67;
-$green-normal: #2faa60;
-$green-dark: #2ca05b;
-
-$blue-light: #2ea8e5;
-$blue-normal: #2d9fd8;
-$blue-dark: #2897ce;
-
-$blue-medium-light: #3498cb;
-$blue-medium: #2f8ebf;
-$blue-medium-dark: #2d86b4;
-
-$orange-light: #fc8a51;
-$orange-normal: #e75e40;
-$orange-dark: #ce5237;
-
-$red-light: #e52c5a;
-$red-normal: #d22852;
-$red-dark: darken($red-normal, 5%);
-
-$black: #000;
-$black-transparent: rgba(0, 0, 0, 0.3);
-
-$border-white-light: #f1f2f4;
-$border-white-normal: #d6dae2;
-$border-white-dark: #c6cacf;
-
-$border-gray-light: #dcdcdc;
-$border-gray-normal: #d7d7d7;
-$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: #fc6d26;
-$border-orange-normal: #ce5237;
-$border-orange-dark: #c14e35;
-
-$border-red-light: #d22852;
-$border-red-normal: #ca264f;
-$border-red-dark: darken($border-red-normal, 5%);
-
-$help-well-bg: #fafafa;
-$help-well-border: #e5e5e5;
-
-$warning-message-bg: #fbf2d9;
-$warning-message-color: #9e8e60;
-$warning-message-border: #f0e2bb;
-
/* tanuki logo colors */
$tanuki-red: #e24329;
$tanuki-orange: #fc6d26;
@@ -186,7 +186,7 @@ $line-removed-dark: #fac5cd;
$line-number-old: #f9d7dc;
$line-number-new: #ddfbe6;
$line-number-select: #fbf2da;
-$match-line: #fafafa;
+$match-line: $gray-light;
$table-border-gray: #f0f0f0;
$line-target-blue: #eaf3fc;
$line-select-yellow: #fcf8e7;
@@ -267,7 +267,13 @@ $zen-control-hover-color: #111;
$calendar-header-color: #b8b8b8;
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
-$calendar-unselectable-bg: #faf9f9;
+$calendar-unselectable-bg: $gray-light;
+
+/*
+ * Cycle Analytics
+ */
+$cycle-analytics-box-padding: 30px;
+$cycle-analytics-box-text-color: #8c8c8c;
/*
* Personal Access Tokens
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 5faedfedd66..9282e0ae03b 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -93,11 +93,8 @@
}
.award-control {
- margin-right: 5px;
- margin-bottom: 5px;
- padding-left: 5px;
- padding-right: 5px;
- line-height: 20px;
+ margin: 3px 5px 3px 0;
+ padding: 6px 5px;
outline: 0;
&:hover,
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 037278bb083..ecc5b24e360 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -1,3 +1,4 @@
+lex
[v-cloak] {
display: none;
}
@@ -18,6 +19,10 @@
}
}
+.is-ghost {
+ opacity: 0.3;
+}
+
.dropdown-menu-issues-board-new {
width: 320px;
@@ -34,47 +39,13 @@
> p {
margin: 0;
font-size: 14px;
- color: #9c9c9c;
}
}
.issue-boards-page {
- .content-wrapper {
- display: -webkit-flex;
- display: flex;
- -webkit-flex-direction: column;
- flex-direction: column;
- }
-
- .sub-nav,
- .issues-filters {
- -webkit-flex: none;
- flex: none;
- }
-
.page-with-sidebar {
- display: -webkit-flex;
- display: flex;
- min-height: 100vh;
- max-height: 100vh;
padding-bottom: 0;
}
-
- .issue-boards-content {
- display: -webkit-flex;
- display: flex;
- -webkit-flex: 1;
- flex: 1;
- width: 100%;
-
- .content {
- display: -webkit-flex;
- display: flex;
- -webkit-flex-direction: column;
- flex-direction: column;
- width: 100%;
- }
- }
}
.boards-app-loading {
@@ -83,46 +54,38 @@
}
.boards-list {
- display: -webkit-flex;
- display: flex;
- -webkit-flex: 1;
- flex: 1;
- -webkit-flex-basis: 0;
- flex-basis: 0;
- min-height: calc(100vh - 152px);
- max-height: calc(100vh - 152px);
+ height: calc(100vh - 152px);
+ width: 100%;
padding-top: 25px;
+ padding-bottom: 25px;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
overflow-x: scroll;
+ white-space: nowrap;
@media (min-width: $screen-sm-min) {
+ height: 475px; // Needed for PhantomJS
+ height: calc(100vh - 220px);
min-height: 475px;
- max-height: none;
}
}
.board {
- display: -webkit-flex;
- display: flex;
- min-width: calc(85vw - 15px);
- max-width: calc(85vw - 15px);
- margin-bottom: 25px;
+ display: inline-block;
+ width: calc(85vw - 15px);
+ height: 100%;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
+ white-space: normal;
+ vertical-align: top;
@media (min-width: $screen-sm-min) {
- min-width: 400px;
- max-width: 400px;
+ width: 400px;
}
}
.board-inner {
- display: -webkit-flex;
- display: flex;
- -webkit-flex-direction: column;
- flex-direction: column;
- width: 100%;
+ height: 100%;
font-size: $issue-boards-font-size;
background: $background-color;
border: 1px solid $border-color;
@@ -193,45 +156,31 @@
}
.board-list {
- -webkit-flex: 1;
- flex: 1;
- height: 400px;
+ height: calc(100% - 49px);
margin-bottom: 0;
padding: 5px;
+ list-style: none;
overflow-y: scroll;
overflow-x: hidden;
}
.board-list-loading {
margin-top: 10px;
- font-size: 26px;
-}
-
-.is-ghost {
- opacity: 0.3;
+ font-size: (26px / $issue-boards-font-size) * 1em;
}
.card {
position: relative;
- width: 100%;
padding: 10px $gl-padding;
background: #fff;
border-radius: $border-radius-default;
box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5);
list-style: none;
- &.user-can-drag {
- padding-left: $gl-padding;
- }
-
&:not(:last-child) {
margin-bottom: 5px;
}
- a {
- cursor: pointer;
- }
-
.label {
border: 0;
outline: 0;
@@ -248,6 +197,7 @@
a {
color: inherit;
+ word-wrap: break-word;
}
}
@@ -256,14 +206,13 @@
line-height: 25px;
.label {
- margin-right: 4px;
+ margin-right: 5px;
font-size: (14px / $issue-boards-font-size) * 1em;
}
}
.card-number {
- margin-right: 8px;
- font-weight: 500;
+ margin-right: 5px;
}
.issue-boards-search {
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index cee198691c2..194a39a8377 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -48,12 +48,6 @@
margin-bottom: 10px;
}
}
-
- .page-sidebar-collapsed {
- .scroll-controls {
- left: 70px;
- }
- }
}
.build-header {
@@ -108,11 +102,29 @@
}
.blocks-container {
- padding: $gl-padding;
+ padding: 0 $gl-padding;
}
.block {
width: 100%;
+
+ &.coverage {
+ padding: 0 16px 11px;
+ }
+
+ .btn-group-justified {
+ margin-top: 5px;
+ }
+ }
+
+ .js-build-variable {
+ color: $code-color;
+ }
+
+ .js-build-value {
+ padding: 2px 4px;
+ color: $black;
+ background-color: $white-light;
}
.build-sidebar-header {
@@ -123,6 +135,13 @@
}
}
+ .retry-link {
+ color: $gl-link-color;
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
.stage-item {
cursor: pointer;
@@ -132,7 +151,7 @@
}
.build-dropdown {
- padding: 0 $gl-padding;
+ padding: $gl-padding 0;
.dropdown-menu-toggle {
margin-top: 8px;
@@ -146,7 +165,6 @@
}
.builds-container {
- margin-top: $gl-padding;
background-color: $white-light;
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
@@ -200,6 +218,9 @@
.build-detail-row {
margin-bottom: 5px;
+ &:last-of-type {
+ margin-bottom: 0;
+ }
}
.build-light-text {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 6a58b445afa..dc57a837155 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -18,8 +18,7 @@
}
.commit-row-title {
- line-height: 1;
- margin-bottom: 7px;
+ line-height: 1.35;
.notes_count {
float: right;
@@ -43,6 +42,7 @@
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
+ line-height: 1;
&:hover {
background-color: darken($gray-light, 10%);
@@ -113,11 +113,13 @@
.commit-row-description {
font-size: 14px;
- border-left: 1px solid #eee;
+ border-left: 1px solid $btn-gray-hover;
padding: 10px 15px;
margin: 10px 0;
- background: #f9f9f9;
+ background: $gray-light;
display: none;
+ white-space: pre-line;
+ word-break: normal;
pre {
border: none;
@@ -134,7 +136,7 @@
.commit-row-info {
color: $gl-gray;
- line-height: 1;
+ line-height: 1.35;
a {
color: $gl-gray;
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
new file mode 100644
index 00000000000..778471a34d7
--- /dev/null
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -0,0 +1,144 @@
+#cycle-analytics {
+ margin: 24px auto 0;
+ max-width: 800px;
+ position: relative;
+
+ .panel {
+
+ .content-block {
+ padding: 24px 0;
+ border-bottom: none;
+ position: relative;
+
+ @media (max-width: $screen-sm-min) {
+ padding: 6px 0 24px;
+ }
+ }
+
+ .column {
+ text-align: center;
+
+ @media (max-width: $screen-sm-min) {
+ padding: 15px 0;
+ }
+
+ .header {
+ font-size: 30px;
+ line-height: 38px;
+ font-weight: normal;
+ margin: 0;
+ }
+
+ .text {
+ color: $layout-link-gray;
+ margin: 0;
+ }
+
+ &:last-child {
+ text-align: right;
+
+ @media (max-width: $screen-sm-min) {
+ text-align: center;
+ }
+ }
+ }
+
+ .dropdown {
+ top: 13px;
+ }
+ }
+
+ .bordered-box {
+ border: 1px solid $border-color;
+ @include border-radius($border-radius-default);
+
+ }
+
+ .content-list {
+ li {
+ padding: 18px $gl-padding $gl-padding;
+
+ .container-fluid {
+ padding: 0;
+ }
+ }
+
+ .title-col {
+ p {
+ margin: 0;
+
+ &.title {
+ line-height: 19px;
+ font-size: 15px;
+ font-weight: 600;
+ color: $gl-title-color;
+ }
+
+ &.text {
+ color: $layout-link-gray;
+
+ &.value-col {
+ color: $gl-title-color;
+ }
+ }
+ }
+ }
+
+ .value-col {
+ text-align: right;
+
+ span {
+ position: relative;
+ vertical-align: middle;
+ top: 3px;
+ }
+ }
+ }
+
+ .landing {
+ margin-bottom: $gl-padding;
+ overflow: hidden;
+
+ .dismiss-icon {
+ position: absolute;
+ right: $cycle-analytics-box-padding;
+ cursor: pointer;
+ color: #b2b2b2;
+ }
+
+ .svg-container {
+ text-align: center;
+
+ svg {
+ width: 136px;
+ height: 136px;
+ }
+ }
+
+ .inner-content {
+ @media (max-width: $screen-sm-min) {
+ padding: 0 28px;
+ text-align: center;
+ }
+
+ h4 {
+ color: $gl-text-color;
+ font-size: 17px;
+ }
+
+ p {
+ color: $cycle-analytics-box-text-color;
+ margin-bottom: $gl-padding;
+ }
+ }
+ }
+
+ .fa-spinner {
+ font-size: 28px;
+ position: relative;
+ margin-left: -20px;
+ left: 50%;
+ margin-top: 36px;
+ }
+
+}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 21cee2e3a70..b8ef76cc74e 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -68,6 +68,11 @@
border-collapse: separate;
margin: 0;
padding: 0;
+ table-layout: fixed;
+
+ .diff-line-num {
+ width: 50px;
+ }
.line_holder td {
line-height: $code_line_height;
@@ -98,10 +103,6 @@
}
tr.line_holder.parallel {
- .old_line, .new_line {
- min-width: 50px;
- }
-
td.line_content.parallel {
width: 46%;
}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 1aa4e06d975..e1304335271 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -59,6 +59,7 @@
}
.encoding-selector,
+ .soft-wrap-toggle,
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector {
@@ -67,6 +68,24 @@
font-family: $regular_font;
}
+ .soft-wrap-toggle {
+ margin: 0 $btn-side-margin;
+ .soft-wrap {
+ display: block;
+ }
+ .no-wrap {
+ display: none;
+ }
+ &.soft-wrap-active {
+ .soft-wrap {
+ display: none;
+ }
+ .no-wrap {
+ display: block;
+ }
+ }
+ }
+
.gitignore-selector, .license-selector, .gitlab-ci-yml-selector {
.dropdown {
line-height: 21px;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 0cd45fb90bf..1d00da1266c 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -60,7 +60,7 @@
pre {
border: none;
- background: #f9f9f9;
+ background: $gray-light;
border-radius: 0;
color: #777;
margin: 0 20px;
@@ -92,7 +92,7 @@
border: 1px solid #eee;
padding: 5px;
@include border-radius(5px);
- background: #f9f9f9;
+ background: $gray-light;
margin-left: 10px;
top: -6px;
img {
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index b657ca47d38..732dc645c66 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -55,3 +55,16 @@
}
}
}
+
+.groups-header {
+
+ @media (min-width: $screen-sm-min) {
+ .nav-links {
+ width: 35%;
+ }
+
+ .nav-controls {
+ width: 65%;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 46c4a11aa2e..41079b6eeb5 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -206,7 +206,7 @@
padding-top: 0;
.block {
- width: $sidebar_collapsed_width - 1px;
+ width: $sidebar_collapsed_width - 2px;
margin-left: -19px;
padding: 15px 0 0;
border-bottom: none;
@@ -404,3 +404,18 @@
margin-bottom: $gl-padding;
}
}
+
+.issuable-list {
+ li {
+ .issue-check {
+ float: left;
+ padding-right: $gl-padding;
+ margin-bottom: 10px;
+ min-width: 15px;
+
+ .selected_issue {
+ vertical-align: text-top;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 3a3bb10feac..3ac34cbc829 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -7,24 +7,9 @@
margin-bottom: 2px;
}
- .issue-check {
- float: left;
- padding-right: 8px;
- margin-bottom: 10px;
- min-width: 15px;
-
- .selected_issue {
- vertical-align: text-top;
- }
- }
-
.issue-labels {
display: inline-block;
}
-
- .issue-no-comments {
- opacity: 0.5;
- }
}
}
@@ -48,6 +33,15 @@ form.edit-issue {
margin: 0;
}
+ul.related-merge-requests > li {
+ display: -ms-flexbox;
+ display: -webkit-flex;
+ display: flex;
+ .merge-request-id {
+ flex-shrink: 0;
+ }
+}
+
.merge-requests-title, .related-branches-title {
font-size: 16px;
font-weight: 600;
@@ -72,12 +66,12 @@ form.edit-issue {
}
&.closed {
- background: #f9f9f9;
+ background: $gray-light;
border-color: #e5e5e5;
}
&.merged {
- background: #f9f9f9;
+ background: $gray-light;
border-color: #e5e5e5;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 606459f82cd..38c7cd98e41 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -7,6 +7,7 @@
display: inline-block;
margin-right: 10px;
margin-bottom: 10px;
+ text-decoration: none;
}
&.suggest-colors-dropdown {
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 1f499897c16..5ec660799e3 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -16,7 +16,7 @@ $colors: (
white_button_origin_chosen : #268ced,
white_header_not_chosen : #f0f0f0,
- white_line_not_chosen : #f9f9f9,
+ white_line_not_chosen : $gray-light,
dark_header_head_neutral : rgba(#3f3, .2),
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index ecfed2c32f1..89485c745d6 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -231,10 +231,6 @@
.merge-request-labels {
display: inline-block;
}
-
- .merge-request-no-comments {
- opacity: 0.5;
- }
}
.merge-request-angle {
@@ -379,13 +375,42 @@
}
}
-.mr-version-switch {
+.mr-version-controls {
background: $background-color;
- padding: $gl-btn-padding;
- color: $gl-placeholder-color;
+ border-bottom: 1px solid $border-color;
+ color: $gl-text-color;
+
+ .mr-version-menus-container {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-align-items: center;
+ align-items: center;
+ padding: 16px;
+ }
+
+ .comments-disabled-notif {
+ padding: 10px 16px;
+ .btn {
+ margin-left: 5px;
+ }
+ }
+
+ .mr-version-dropdown,
+ .mr-version-compare-dropdown {
+ margin: 0 7px;
+ }
+
+ .comments-disabled-notif {
+ border-top: 1px solid $border-color;
+ }
+
+ .dropdown-title {
+ color: $gl-text-color;
+ }
- a.btn-link {
- color: $gl-dark-link-color;
+ .fa-info-circle {
+ color: $orange-normal;
+ padding-right: 5px;
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index b94f524b513..8c2ba3ed58c 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -2,13 +2,17 @@
max-width: 90%;
}
-li.milestone {
- h4 {
- font-weight: bold;
- }
+.milestones {
+ .milestone {
+ padding: 10px 16px;
+
+ h4 {
+ font-weight: bold;
+ }
- .progress {
- height: 6px;
+ .progress {
+ height: 6px;
+ }
}
}
@@ -29,6 +33,7 @@ li.milestone {
// Issue title
span a {
color: $gl-text-color;
+ word-wrap: break-word;
}
}
}
@@ -64,3 +69,14 @@ li.milestone {
border-bottom: 1px solid $border-color;
padding: 20px 0;
}
+
+@media (max-width: $screen-sm-min) {
+ .milestone-actions {
+ @include clearfix();
+ padding-top: $gl-vert-padding;
+
+ .btn:first-child {
+ margin-left: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index ee5d9de66d8..b035bfc9f3c 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -147,14 +147,41 @@
}
.stage-cell {
- text-align: center;
+ font-size: 0;
svg {
height: 18px;
width: 18px;
+ position: relative;
+ z-index: 2;
vertical-align: middle;
overflow: visible;
}
+
+ .stage-container {
+ display: inline-block;
+ position: relative;
+ margin-right: 6px;
+
+ .tooltip {
+ white-space: nowrap;
+ }
+
+ &:not(:last-child) {
+ &::after {
+ content: '';
+ width: 8px;
+ position: absolute;;
+ right: -7px;
+ bottom: 8px;
+ border-bottom: 2px solid $border-color;
+ }
+ }
+
+ a {
+ display: block;
+ }
+ }
}
.duration,
@@ -318,9 +345,17 @@
.build-content {
width: 130px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+
+ .ci-status-text {
+ width: 110px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ display: inline-block;
+ position: relative;
+ top: -1px;
+ }
a {
color: $layout-link-gray;
@@ -331,13 +366,74 @@
text-decoration: underline;
}
}
+ }
+
+ .dropdown-menu-toggle {
+ border: none;
+ width: auto;
+ padding: 0;
+ color: $layout-link-gray;
+
+ .ci-status-text {
+ width: 80px;
+ }
+ }
+
+ .grouped-pipeline-dropdown {
+ padding: 8px 0;
+ width: 200px;
+ left: auto;
+ right: -214px;
+ top: -9px;
+
+ a:hover {
+ .ci-status-text {
+ text-decoration: none;
+ }
+ }
+
+ .ci-status-text {
+ width: 145px;
+ }
+ .arrow {
+ &:before,
+ &:after {
+ content: '';
+ display: inline-block;
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ top: 18px;
+ }
+
+ &:before {
+ left: -5px;
+ margin-top: -6px;
+ border-width: 7px 5px 7px 0;
+ border-right-color: $border-color;
+ }
+
+ &:after {
+ left: -4px;
+ margin-top: -9px;
+ border-width: 10px 7px 10px 0;
+ border-right-color: $white-light;
+ }
+ }
+ }
+
+ .badge {
+ background-color: $gray-dark;
+ color: $layout-link-gray;
+ font-weight: normal;
}
}
svg {
- position: relative;
- top: 2px;
+ vertical-align: middle;
margin-right: 5px;
}
@@ -442,7 +538,7 @@
width: 21px;
height: 25px;
position: absolute;
- top: -28.5px;
+ top: -29px;
border-top: 2px solid $border-color;
}
@@ -490,6 +586,6 @@
.ci-status-icon-created {
svg {
- fill: $table-text-gray;
+ fill: $gray-darkest;
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 6f58203f49c..0fcdaf94a21 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -93,8 +93,9 @@
.profile-user-bio {
// Limits the width of the user bio for readability.
- max-width: 750px;
- margin: auto;
+ max-width: 600px;
+ margin: 15px auto 0;
+ padding: 0 16px;
}
.user-avatar-button {
@@ -212,6 +213,28 @@
}
.user-profile {
+ .cover-controls a {
+ margin-left: 5px;
+ }
+ .profile-header {
+ margin: 0 auto;
+ .avatar-holder {
+ width: 90px;
+ display: inline-block;
+ }
+ .user-info {
+ display: inline-block;
+ text-align: left;
+ vertical-align: middle;
+ margin-left: 15px;
+ .handle {
+ color: $gl-gray-light;
+ }
+ .member-date {
+ margin-bottom: 4px;
+ }
+ }
+ }
@media (max-width: $screen-xs-max) {
.cover-block {
padding-top: 20px;
@@ -219,16 +242,26 @@
.cover-controls {
position: static;
+ padding: 0 16px;
margin-bottom: 20px;
+ display: -webkit-flex;
+ display: flex;
.btn {
- display: inline-block;
- width: 46%;
+ -webkit-flex-grow: 1;
+ flex-grow: 1;
+ &:first-child {
+ margin-left: 0;
+ }
}
}
}
}
+.user-profile-nav {
+ margin-top: 15px;
+}
+
table.u2f-registrations {
th:not(:last-child), td:not(:last-child) {
border-right: solid 1px transparent;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index f2db373da52..78bc4b79e86 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -334,6 +334,10 @@ a.deploy-project-label {
a {
color: $gl-dark-link-color;
}
+
+ .dropdown-menu {
+ width: 240px;
+ }
}
.last-push-widget {
@@ -723,9 +727,15 @@ pre.light-well {
}
}
-.project-refs-form {
- .dropdown-menu {
- width: 300px;
+.project-refs-form .dropdown-menu, .dropdown-menu-projects {
+ width: 300px;
+
+ @media (min-width: $screen-sm-min) {
+ width: 500px;
+ }
+
+ a {
+ white-space: normal;
}
}
@@ -733,6 +743,62 @@ pre.light-well {
.dropdown-menu {
width: 300px;
}
+
+ &.from .compare-dropdown-toggle {
+ width: 237px;
+ }
+
+ &.to .compare-dropdown-toggle {
+ width: 254px;
+ }
+
+ .dropdown-toggle-text {
+ display: block;
+ height: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 100%;
+ }
+}
+
+.compare-ellipsis {
+ display: inline;
+}
+
+@media (max-width: $screen-xs-max) {
+ .compare-form-group {
+ .input-group {
+ width: 100%;
+
+ & > .compare-dropdown-toggle {
+ width: 100%;
+ }
+ }
+
+ .dropdown-menu {
+ width: 100%;
+ }
+ }
+
+ .compare-switch-container {
+ text-align: center;
+ padding: 0 0 $gl-padding;
+
+ .commits-compare-switch {
+ float: none;
+ }
+ }
+
+ .compare-ellipsis {
+ display: block;
+ text-align: center;
+ padding: 0 0 $gl-padding;
+ }
+
+ .commits-compare-btn {
+ width: 100%;
+ }
}
.clearable-input {
@@ -760,3 +826,13 @@ pre.light-well {
}
}
}
+
+.project-path {
+ .form-control {
+ min-width: 100px;
+ }
+ .select2-choice {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 436fb00ba2e..e77f9816d8a 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -103,7 +103,7 @@
// Custom dropdown positioning
.dropdown-menu {
- top: 30px;
+ top: 37px;
left: -5px;
padding: 0;
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 2aa939b7dc3..857eb76131a 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -2,20 +2,6 @@
padding: 2px;
}
-.snippet-holder {
- margin-bottom: -$gl-padding;
-
- .file-holder {
- border-top: 0;
- }
-
- .file-actions {
- .btn-clipboard {
- @extend .btn;
- }
- }
-}
-
.markdown-snippet-copy {
position: fixed;
top: -10px;
@@ -24,29 +10,25 @@
max-width: 0;
}
-.file-holder.snippet-file-content {
- padding-bottom: $gl-padding;
- border-bottom: 1px solid $border-color;
-
- .file-title {
- padding-top: $gl-padding;
- padding-bottom: $gl-padding;
- }
+.snippet-file-content {
+ border-radius: 3px;
+ margin-bottom: $gl-padding;
- .file-actions {
- top: 12px;
+ .btn-clipboard {
+ @extend .btn;
}
+}
- .file-content {
- border-left: 1px solid $border-color;
- border-right: 1px solid $border-color;
- border-bottom: 1px solid $border-color;
- }
+.project-snippets .awards {
+ border-bottom: 1px solid $table-border-color;
+ padding-bottom: $gl-padding;
}
.snippet-title {
font-size: 24px;
- font-weight: normal;
+ font-weight: 600;
+ padding: $gl-padding;
+ padding-left: 0;
}
.snippet-actions {
@@ -54,3 +36,7 @@
float: right;
}
}
+
+.snippet-scope-menu .btn-new {
+ margin-top: 15px;
+}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 0340526a53a..68a5d1ae06c 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -99,7 +99,7 @@
pre {
border: none;
- background: #f9f9f9;
+ background: $gray-light;
border-radius: 0;
color: #777;
margin: 0 20px;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 538f211c65b..41ad10f07bd 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -11,14 +11,8 @@
}
}
- .last-commit {
- max-width: 506px;
-
- .last-commit-content {
- @include str-truncated;
- width: calc(100% - 140px);
- margin-left: 3px;
- }
+ .add-to-tree {
+ vertical-align: top;
}
.tree-table {
@@ -32,6 +26,20 @@
line-height: 21px;
}
+ .last-commit {
+ @include str-truncated(506px);
+
+ @media (min-width: $screen-sm-max) and (max-width: $screen-md-max) {
+ @include str-truncated(450px);
+ }
+
+ }
+
+ .commit-history-link-spacer {
+ margin: 0 10px;
+ color: $table-border-color;
+ }
+
&:hover {
td {
background-color: $row-hover;
@@ -52,6 +60,15 @@
}
.tree-item {
+ .link-container {
+ padding: 0;
+
+ a {
+ padding: 10px $gl-padding;
+ display: block;
+ }
+ }
+
.tree-item-file-name {
max-width: 320px;
vertical-align: middle;
@@ -87,11 +104,17 @@
}
}
- .tree_commit {
- color: $gl-gray;
+ .tree-time-ago {
+ min-width: 135px;
+ color: $gl-gray-light;
+ }
+
+ .tree-commit {
+ max-width: 320px;
+ color: $gl-gray-light;
.tree-commit-link {
- color: $gl-gray;
+ color: $gl-gray-light;
&:hover {
text-decoration: underline;
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 8d855ce99b0..c9846103762 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -20,6 +20,9 @@
$l-cyan: #8abeb7;
$l-white: $ci-text-color;
+ .term-bold {
+ font-weight: bold;
+ }
.term-italic {
font-style: italic;
}
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index cdfa8d91a28..aa7570cd896 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -10,7 +10,7 @@ class Admin::GroupsController < Admin::ApplicationController
def show
@members = @group.members.order("access_level DESC").page(params[:members_page])
- @requesters = @group.requesters
+ @requesters = AccessRequestsFinder.new(@group).execute(current_user)
@projects = @group.projects.page(params[:projects_page])
end
@@ -60,6 +60,14 @@ class Admin::GroupsController < Admin::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level, :request_access_enabled)
+ params.require(:group).permit(
+ :avatar,
+ :description,
+ :lfs_enabled,
+ :name,
+ :path,
+ :request_access_enabled,
+ :visibility_level
+ )
end
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 0d2f4f6eb38..1d963bdf7d5 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -22,7 +22,7 @@ class Admin::ProjectsController < Admin::ApplicationController
end
@project_members = @project.members.page(params[:project_members_page])
- @requesters = @project.requesters
+ @requesters = AccessRequestsFinder.new(@project).execute(current_user)
end
def transfer
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index a7af3cb8345..78012960252 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -7,19 +7,15 @@ module Ci
def create
@content = params[:content]
+ @error = Ci::GitlabCiYamlProcessor.validation_message(@content)
+ @status = @error.blank?
- if @content.blank?
- @status = false
- @error = "Please provide content of .gitlab-ci.yml"
- else
+ if @error.blank?
@config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
- @status = true
+ @jobs = @config_processor.jobs
end
- rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
- @error = e.message
- @status = false
rescue
@error = 'Undefined error'
@status = false
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index ba07cea569c..d5a8a962662 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -62,6 +62,7 @@ module AuthenticatesWithTwoFactor
session.delete(:otp_user_id)
session.delete(:challenges)
+ remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
flash.now[:alert] = 'Authentication via U2F device failed.'
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index f2b8f297bc2..dacb5679dd3 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -7,8 +7,7 @@ module CreatesCommit
commit_params = @commit_params.merge(
source_project: @project,
source_branch: @ref,
- target_branch: @target_branch,
- previous_path: @previous_path
+ target_branch: @target_branch
)
result = service.new(@tree_edit_project, current_user, commit_params).execute
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index f40b62446e5..bb32bc502e6 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -3,21 +3,54 @@ module IssuableActions
included do
before_action :authorize_destroy_issuable!, only: :destroy
+ before_action :authorize_admin_issuable!, only: :bulk_update
end
def destroy
issuable.destroy
+ destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
+ TodoService.new.public_send(destroy_method, issuable, current_user)
name = issuable.class.name.titleize.downcase
flash[:notice] = "The #{name} was successfully deleted."
redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
end
+ def bulk_update
+ result = Issuable::BulkUpdateService.new(project, current_user, bulk_update_params).execute(resource_name)
+ quantity = result[:count]
+
+ render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" }
+ end
+
private
def authorize_destroy_issuable!
- unless current_user.can?(:"destroy_#{issuable.to_ability_name}", issuable)
+ unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
return access_denied!
end
end
+
+ def authorize_admin_issuable!
+ unless can?(current_user, :"admin_#{resource_name}", @project)
+ return access_denied!
+ end
+ end
+
+ def bulk_update_params
+ params.require(:update).permit(
+ :issuable_ids,
+ :assignee_id,
+ :milestone_id,
+ :state_event,
+ :subscription_event,
+ label_ids: [],
+ add_label_ids: [],
+ remove_label_ids: []
+ )
+ end
+
+ def resource_name
+ @resource_name ||= controller_name.singularize
+ end
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index 52682ef9dc9..b8ed2c159a7 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -1,6 +1,5 @@
module MembershipActions
extend ActiveSupport::Concern
- include MembersHelper
def request_access
membershipable.request_access(current_user)
@@ -10,11 +9,7 @@ module MembershipActions
end
def approve_access_request
- @member = membershipable.requesters.find(params[:id])
-
- return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
-
- @member.accept_request
+ Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
redirect_to polymorphic_url([membershipable, :members])
end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index a69877edfd4..4cb3be41064 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -13,7 +13,7 @@ module ServiceParams
# `issue_events` and `merge_request_events` (singular!)
# See app/helpers/services_helper.rb for how we
# make those event names plural as special case.
- :issues_events, :merge_requests_events,
+ :issues_events, :confidential_issues_events, :merge_requests_events,
:notify_only_broken_builds, :notify_only_broken_pipelines,
:add_pusher, :send_from_committer_email, :disable_diffs,
:external_wiki_url, :notify, :color,
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index 172d5344b7a..3717c49f272 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -10,7 +10,9 @@ module ToggleAwardEmoji
if awardable.user_can_award?(current_user, name)
awardable.toggle_award_emoji(name, current_user)
- TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
+
+ todoable = to_todoable(awardable)
+ TodoService.new.new_award_emoji(todoable, current_user) if todoable
render json: { ok: true }
else
@@ -24,8 +26,10 @@ module ToggleAwardEmoji
case awardable
when Note
awardable.noteable
- else
+ when MergeRequest, Issue
awardable
+ when Snippet
+ nil
end
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 272164cd0cc..9c323d7705a 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -15,7 +15,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
@members = @members.order('access_level DESC').page(params[:page]).per(50)
- @requesters = @group.requesters if can?(current_user, :admin_group, @group)
+ @requesters = AccessRequestsFinder.new(@group).execute(current_user)
@group_member = @group.group_members.new
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index cb82d62616c..b83c3a872cf 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -121,7 +121,17 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock, :request_access_enabled)
+ params.require(:group).permit(
+ :avatar,
+ :description,
+ :lfs_enabled,
+ :name,
+ :path,
+ :public,
+ :request_access_enabled,
+ :share_with_group_lock,
+ :visibility_level
+ )
end
def load_events
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 7e8597a5eb3..256c41e6145 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -1,18 +1,17 @@
class Import::BaseController < ApplicationController
private
- def get_or_create_namespace
+ def find_or_create_namespace(name, owner)
+ return current_user.namespace if name == owner
+ return current_user.namespace unless current_user.can_create_group?
+
begin
- namespace = Group.create!(name: @target_namespace, path: @target_namespace, owner: current_user)
+ name = params[:target_namespace].presence || name
+ namespace = Group.create!(name: name, path: name, owner: current_user)
namespace.add_owner(current_user)
+ namespace
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
- namespace = Namespace.find_by_path_or_name(@target_namespace)
- unless current_user.can?(:create_projects, namespace)
- @already_been_taken = true
- return false
- end
+ Namespace.find_by_path_or_name(name)
end
-
- namespace
end
end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 944c73d139a..6ea54744da8 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -35,23 +35,20 @@ class Import::BitbucketController < Import::BaseController
end
def create
- @repo_id = params[:repo_id] || ""
- repo = client.project(@repo_id.gsub("___", "/"))
- @project_name = repo["slug"]
-
- repo_owner = repo["owner"]
- repo_owner = current_user.username if repo_owner == client.user["user"]["username"]
- @target_namespace = params[:new_namespace].presence || repo_owner
-
- namespace = get_or_create_namespace || (render and return)
+ @repo_id = params[:repo_id].to_s
+ repo = client.project(@repo_id.gsub('___', '/'))
+ @project_name = repo['slug']
+ @target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username'])
unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute
- @access_denied = true
- render
- return
+ render 'deploy_key' and return
end
- @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute
+ if current_user.can?(:create_projects, @target_namespace)
+ @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
+ else
+ render 'unauthorized'
+ end
end
private
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 9c1b0eb20f4..ee7d498c59c 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -40,15 +40,15 @@ class Import::GithubController < Import::BaseController
def create
@repo_id = params[:repo_id].to_i
repo = client.repo(@repo_id)
- @project_name = repo.name
-
- repo_owner = repo.owner.login
- repo_owner = current_user.username if repo_owner == client.user.login
- @target_namespace = params[:new_namespace].presence || repo_owner
-
- namespace = get_or_create_namespace || (render and return)
-
- @project = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute
+ @project_name = params[:new_name].presence || repo.name
+ namespace_path = params[:target_namespace].presence || current_user.namespace_path
+ @target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
+
+ if current_user.can?(:create_projects, @target_namespace)
+ @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute
+ else
+ render 'unauthorized'
+ end
end
private
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index 08130ee8176..73837ffbe67 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -26,15 +26,14 @@ class Import::GitlabController < Import::BaseController
def create
@repo_id = params[:repo_id].to_i
repo = client.project(@repo_id)
- @project_name = repo["name"]
+ @project_name = repo['name']
+ @target_namespace = find_or_create_namespace(repo['namespace']['path'], client.user['username'])
- repo_owner = repo["namespace"]["path"]
- repo_owner = current_user.username if repo_owner == client.user["username"]
- @target_namespace = params[:new_namespace].presence || repo_owner
-
- namespace = get_or_create_namespace || (render and return)
-
- @project = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute
+ if current_user.can?(:create_projects, @target_namespace)
+ @project = Gitlab::GitlabImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
+ else
+ render 'unauthorized'
+ end
end
private
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 66ebdcc37a7..7e4da73bc11 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -11,7 +11,8 @@ class JwtController < ApplicationController
service = SERVICES[params[:service]]
return head :not_found unless service
- result = service.new(@project, @user, auth_params).execute
+ result = service.new(@authentication_result.project, @authentication_result.actor, auth_params).
+ execute(authentication_abilities: @authentication_result.authentication_abilities || [])
render json: result, status: result[:http_status]
end
@@ -19,31 +20,37 @@ class JwtController < ApplicationController
private
def authenticate_project_or_user
- authenticate_with_http_basic do |login, password|
- # if it's possible we first try to authenticate project with login and password
- @project = authenticate_project(login, password)
- return if @project
+ @authentication_result = Gitlab::Auth::Result.new
- @user = authenticate_user(login, password)
- return if @user
+ authenticate_with_http_basic do |login, password|
+ @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
- render_403
+ render_unauthorized unless @authentication_result.success? &&
+ (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
end
+ rescue Gitlab::Auth::MissingPersonalTokenError
+ render_missing_personal_token
end
- def auth_params
- params.permit(:service, :scope, :account, :client_id)
+ def render_missing_personal_token
+ render json: {
+ errors: [
+ { code: 'UNAUTHORIZED',
+ message: "HTTP Basic: Access denied\n" \
+ "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
+ "You can generate one at #{profile_personal_access_tokens_url}" }
+ ] }, status: 401
end
- def authenticate_project(login, password)
- if login == 'gitlab-ci-token'
- Project.with_builds_enabled.find_by(runners_token: password)
- end
+ def render_unauthorized
+ render json: {
+ errors: [
+ { code: 'UNAUTHORIZED',
+ message: 'HTTP Basic: Access denied' }
+ ] }, status: 401
end
- def authenticate_user(login, password)
- user = Gitlab::Auth.find_with_user_password(login, password)
- Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
- user
+ def auth_params
+ params.permit(:service, :scope, :account, :client_id)
end
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index c5fa756d02b..f71e0a1302b 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -73,7 +73,8 @@ class ProfilesController < Profiles::ApplicationController
:skype,
:twitter,
:username,
- :website_url
+ :website_url,
+ :organization
)
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index cdf9a04bacf..b78cc6585ba 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -38,12 +38,7 @@ class Projects::BlobController < Projects::ApplicationController
end
def update
- if params[:file_path].present?
- @previous_path = @path
- @path = params[:file_path]
- @commit_params[:file_path] = @path
- end
-
+ @path = params[:file_path] if params[:file_path].present?
after_edit_path =
if from_merge_request && @target_branch == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
@@ -143,6 +138,8 @@ class Projects::BlobController < Projects::ApplicationController
params[:file_name] = params[:file].original_filename
end
File.join(@path, params[:file_name])
+ elsif params[:file_path].present?
+ params[:file_path]
else
@path
end
@@ -155,6 +152,7 @@ class Projects::BlobController < Projects::ApplicationController
@commit_params = {
file_path: @file_path,
commit_message: params[:commit_message],
+ previous_path: @path,
file_content: params[:content],
file_content_encoding: params[:encoding],
last_commit_sha: params[:last_commit_sha]
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 9404612a993..4aa7982eab4 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -33,7 +33,7 @@ module Projects
def issue
@issue ||=
- IssuesFinder.new(current_user, project_id: project.id, state: 'all')
+ IssuesFinder.new(current_user, project_id: project.id)
.execute
.where(iid: params[:id])
.first!
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 12195c3cbb8..3b2e35a7a05 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -35,7 +35,11 @@ class Projects::BuildsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
- render json: @build.to_json(methods: :trace_html)
+ render json: {
+ id: @build.id,
+ status: @build.status,
+ trace_html: @build.trace_html
+ }
end
end
end
@@ -74,12 +78,12 @@ class Projects::BuildsController < Projects::ApplicationController
def erase
@build.erase(erased_by: current_user)
redirect_to namespace_project_build_path(project.namespace, project, @build),
- notice: "Build has been sucessfully erased!"
+ notice: "Build has been successfully erased!"
end
def raw
- if @build.has_trace?
- send_file @build.path_to_trace, type: 'text/plain; charset=utf-8', disposition: 'inline'
+ if @build.has_trace_file?
+ send_file @build.trace_file_path, type: 'text/plain; charset=utf-8', disposition: 'inline'
else
render_404
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 02fb3f56890..cdfc1ba7b92 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -10,10 +10,11 @@ class Projects::CommitController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds]
before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
+ before_action :authorize_read_pipeline!, only: [:pipelines]
before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
- before_action :define_commit_vars, only: [:show, :diff_for_path, :builds]
- before_action :define_status_vars, only: [:show, :builds]
+ before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines]
+ before_action :define_status_vars, only: [:show, :builds, :pipelines]
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
@@ -31,6 +32,9 @@ class Projects::CommitController < Projects::ApplicationController
render_diff_for_path(@commit.diffs(diff_options))
end
+ def pipelines
+ end
+
def builds
end
@@ -96,10 +100,6 @@ class Projects::CommitController < Projects::ApplicationController
@noteable = @commit ||= @project.commit(params[:id])
end
- def pipelines
- @pipelines ||= project.pipelines.where(sha: commit.sha)
- end
-
def ci_builds
@ci_builds ||= Ci::Build.where(pipeline: pipelines)
end
@@ -134,8 +134,9 @@ class Projects::CommitController < Projects::ApplicationController
end
def define_status_vars
- @statuses = CommitStatus.where(pipeline: pipelines).relevant
- @builds = Ci::Build.where(pipeline: pipelines).relevant
+ @ci_pipelines = project.pipelines.where(sha: commit.sha)
+ @statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant
+ @builds = Ci::Build.where(pipeline: @ci_pipelines).relevant
end
def assign_change_commit_vars(mr_source_branch)
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
new file mode 100644
index 00000000000..16a7b1fc6e2
--- /dev/null
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -0,0 +1,67 @@
+class Projects::CycleAnalyticsController < Projects::ApplicationController
+ include ActionView::Helpers::DateHelper
+ include ActionView::Helpers::TextHelper
+
+ before_action :authorize_read_cycle_analytics!
+
+ def show
+ @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date)
+
+ respond_to do |format|
+ format.html
+ format.json { render json: cycle_analytics_json }
+ end
+ end
+
+ private
+
+ def parse_start_date
+ case cycle_analytics_params[:start_date]
+ when '30' then 30.days.ago
+ when '90' then 90.days.ago
+ else 90.days.ago
+ end
+ end
+
+ def cycle_analytics_params
+ return {} unless params[:cycle_analytics].present?
+
+ { start_date: params[:cycle_analytics][:start_date] }
+ end
+
+ def cycle_analytics_json
+ cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"],
+ [:plan, "Plan", "Time before an issue starts implementation"],
+ [:code, "Code", "Time until first merge request"],
+ [:test, "Test", "Total test time for all commits/merges"],
+ [:review, "Review", "Time between merge request creation and merge/close"],
+ [:staging, "Staging", "From merge request merge until deploy to production"],
+ [:production, "Production", "From issue creation until deploy to production"]]
+
+ stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)|
+ value = @cycle_analytics.send(stage_method).presence
+
+ stats << {
+ title: stage_text,
+ description: stage_description,
+ value: value && !value.zero? ? distance_of_time_in_words(value) : nil
+ }
+ stats
+ end
+
+ issues = @cycle_analytics.summary.new_issues
+ commits = @cycle_analytics.summary.commits
+ deploys = @cycle_analytics.summary.deploys
+
+ summary = [
+ { title: "New Issue".pluralize(issues), value: issues },
+ { title: "Commit".pluralize(commits), value: commits },
+ { title: "Deploy".pluralize(deploys), value: deploys }
+ ]
+
+ {
+ summary: summary,
+ stats: stats
+ }
+ end
+end
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index a5b4031c30f..383e184d796 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -4,7 +4,11 @@ class Projects::GitHttpClientController < Projects::ApplicationController
include ActionController::HttpAuthentication::Basic
include KerberosSpnegoHelper
- attr_reader :user
+ attr_reader :authentication_result
+
+ delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
+
+ alias_method :user, :actor
# Git clients will not know what authenticity token to send along
skip_before_action :verify_authenticity_token
@@ -15,32 +19,25 @@ class Projects::GitHttpClientController < Projects::ApplicationController
private
def authenticate_user
+ @authentication_result = Gitlab::Auth::Result.new
+
if project && project.public? && download_request?
return # Allow access
end
if allow_basic_auth? && basic_auth_provided?
login, password = user_name_and_password(request)
- auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
-
- if auth_result.type == :ci && download_request?
- @ci = true
- elsif auth_result.type == :oauth && !download_request?
- # Not allowed
- elsif auth_result.type == :missing_personal_token
- render_missing_personal_token
- return # Render above denied access, nothing left to do
- else
- @user = auth_result.user
- end
- if ci? || user
+ if handle_basic_authentication(login, password)
return # Allow access
end
elsif allow_kerberos_spnego_auth? && spnego_provided?
- @user = find_kerberos_user
+ kerberos_user = find_kerberos_user
+
+ if kerberos_user
+ @authentication_result = Gitlab::Auth::Result.new(
+ kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
- if user
send_final_spnego_response
return # Allow access
end
@@ -48,6 +45,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_challenges
render plain: "HTTP Basic: Access denied\n", status: 401
+ rescue Gitlab::Auth::MissingPersonalTokenError
+ render_missing_personal_token
end
def basic_auth_provided?
@@ -114,7 +113,44 @@ class Projects::GitHttpClientController < Projects::ApplicationController
render plain: 'Not Found', status: :not_found
end
+ def handle_basic_authentication(login, password)
+ @authentication_result = Gitlab::Auth.find_for_git_client(
+ login, password, project: project, ip: request.ip)
+
+ return false unless @authentication_result.success?
+
+ if download_request?
+ authentication_has_download_access?
+ else
+ authentication_has_upload_access?
+ end
+ end
+
def ci?
- @ci.present?
+ authentication_result.ci?(project)
+ end
+
+ def lfs_deploy_token?
+ authentication_result.lfs_deploy_token?(project)
+ end
+
+ def authentication_has_download_access?
+ has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code)
+ end
+
+ def authentication_has_upload_access?
+ has_authentication_ability?(:push_code)
+ end
+
+ def has_authentication_ability?(capability)
+ (authentication_abilities || []).include?(capability)
+ end
+
+ def authentication_project
+ authentication_result.project
+ end
+
+ def verify_workhorse_api!
+ Gitlab::Workhorse.verify_api_request!(request.headers)
end
end
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index b4373ef89ef..662d38b10a5 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -1,6 +1,8 @@
# This file should be identical in GitLab Community Edition and Enterprise Edition
class Projects::GitHttpController < Projects::GitHttpClientController
+ before_action :verify_workhorse_api!
+
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs
@@ -56,6 +58,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end
def render_ok
+ set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.git_http_ok(repository, user)
end
@@ -83,7 +86,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end
def access
- @access ||= Gitlab::GitAccess.new(user, project, 'http')
+ @access ||= Gitlab::GitAccess.new(user, project, 'http', authentication_abilities: authentication_abilities)
end
def access_check
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index b5624046387..0ae8ff98009 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -59,6 +59,7 @@ class Projects::HooksController < Projects::ApplicationController
:pipeline_events,
:enable_ssl_verification,
:issues_events,
+ :confidential_issues_events,
:merge_requests_events,
:note_events,
:push_events,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 72d2d361878..ef13e0677d2 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -20,24 +20,12 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow modify issue
before_action :authorize_update_issue!, only: [:edit, :update]
- # Allow issues bulk update
- before_action :authorize_admin_issues!, only: [:bulk_update]
-
respond_to :html
def index
- terms = params['issue_search']
@issues = issues_collection
-
- if terms.present?
- if terms =~ /\A#(\d+)\z/
- @issues = @issues.where(iid: $1)
- else
- @issues = @issues.full_search(terms)
- end
- end
-
@issues = @issues.page(params[:page])
+
@labels = @project.labels.where(title: params[:label_name])
respond_to do |format|
@@ -66,7 +54,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
- raw_notes = @issue.notes_with_associations.fresh
+ raw_notes = @issue.notes.inc_relations_for_view.fresh
@notes = Banzai::NoteRenderer.
render(raw_notes, @project, current_user, @path, @project_wiki, @ref)
@@ -168,16 +156,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- def bulk_update
- result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
-
- respond_to do |format|
- format.json do
- render json: { notice: "#{result[:count]} issues updated" }
- end
- end
- end
-
protected
def issue
@@ -237,17 +215,4 @@ class Projects::IssuesController < Projects::ApplicationController
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
)
end
-
- def bulk_update_params
- params.require(:update).permit(
- :issues_ids,
- :assignee_id,
- :milestone_id,
- :state_event,
- :subscription_event,
- label_ids: [],
- add_label_ids: [],
- remove_label_ids: []
- )
- end
end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 69066cb40e6..9005b104e90 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -3,6 +3,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
before_action :require_lfs_enabled!
before_action :lfs_check_access!
+ before_action :verify_workhorse_api!, only: [:upload_authorize]
def download
lfs_object = LfsObject.find_by_oid(oid)
@@ -15,14 +16,8 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
end
def upload_authorize
- render(
- json: {
- StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload",
- LfsOid: oid,
- LfsSize: size,
- },
- content_type: 'application/json; charset=utf-8'
- )
+ set_workhorse_internal_api_content_type
+ render json: Gitlab::Workhorse.lfs_upload_ok(oid, size)
end
def upload_finalize
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 4f9ca0097a1..68bb4232f5b 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -18,6 +18,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_commit_vars, only: [:diffs]
before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
+ before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
# Allow read any merge_request
before_action :authorize_read_merge_request!
@@ -31,17 +32,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
def index
- terms = params['issue_search']
@merge_requests = merge_requests_collection
-
- if terms.present?
- if terms =~ /\A[#!](\d+)\z/
- @merge_requests = @merge_requests.where(iid: $1)
- else
- @merge_requests = @merge_requests.full_search(terms)
- end
- end
-
@merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project)
@@ -90,16 +81,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.merge_request_diff
end
+ @merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff
+ @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
+
+ if params[:start_sha].present?
+ @start_sha = params[:start_sha]
+ @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
+
+ unless @start_version
+ render_404
+ end
+ end
+
respond_to do |format|
format.html { define_discussion_vars }
format.json do
- unless @merge_request_diff.latest?
- # Disable comments if browsing older version of the diff
- @diff_notes_disabled = true
+ if @start_sha
+ compared_diff_version
+ else
+ original_diff_version
end
- @diffs = @merge_request_diff.diffs(diff_options)
-
render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") }
end
end
@@ -307,8 +309,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return
end
- TodoService.new.merge_merge_request(merge_request, current_user)
-
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present?
@@ -417,17 +417,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def validates_merge_request
- # If source project was removed (Ex. mr from fork to origin)
- return invalid_mr unless @merge_request.source_project
-
# Show git not found page
# if there is no saved commits between source & target branch
if @merge_request.commits.blank?
# and if target branch doesn't exist
return invalid_mr unless @merge_request.target_branch_exists?
-
- # or if source branch doesn't exist
- return invalid_mr unless @merge_request.source_branch_exists?
end
end
@@ -497,7 +491,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def invalid_mr
- # Render special view for MR with removed source or target branch
+ # Render special view for MR with removed target branch
render 'invalid'
end
@@ -529,4 +523,20 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
end
+
+ def compared_diff_version
+ @diff_notes_disabled = true
+ @diffs = @merge_request_diff.compare_with(@start_sha).diffs(diff_options)
+ end
+
+ def original_diff_version
+ @diff_notes_disabled = !@merge_request_diff.latest?
+ @diffs = @merge_request_diff.diffs(diff_options)
+ end
+
+ def close_merge_request_without_source_project
+ if !@merge_request.source_project && @merge_request.open?
+ @merge_request.close
+ end
+ end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index b0c72cfe4b4..371cc3787fb 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -7,11 +7,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
- all_pipelines = project.pipelines
- @pipelines_count = all_pipelines.count
- @running_or_pending_count = all_pipelines.running_or_pending.count
- @pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope)
- @pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30)
+ @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
+
+ @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
+ @pipelines_count = PipelinesFinder.new(project).execute.count
end
def new
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 42a7e5a2c30..2343c7d20ec 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -29,7 +29,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_members = @group_members.order('access_level DESC')
end
- @requesters = @project.requesters if can?(current_user, :admin_project, @project)
+ @requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new
@project_group_links = @project.project_group_links
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 6a227d85f6f..97e6e9471e0 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -20,9 +20,8 @@ class Projects::ServicesController < Projects::ApplicationController
def update
if @service.update_attributes(service_params[:service])
redirect_to(
- edit_namespace_project_service_path(@project.namespace, @project,
- @service.to_param, notice:
- 'Successfully updated.')
+ edit_namespace_project_service_path(@project.namespace, @project, @service.to_param),
+ notice: 'Successfully updated.'
)
else
render 'edit'
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 17ceefec3b8..e290a0eadda 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,6 +1,8 @@
class Projects::SnippetsController < Projects::ApplicationController
+ include ToggleAwardEmoji
+
before_action :module_enabled
- before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
+ before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji]
# Allow read any snippet
before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
@@ -80,6 +82,7 @@ class Projects::SnippetsController < Projects::ApplicationController
def snippet
@snippet ||= @project.snippets.find(params[:id])
end
+ alias_method :awardable, :snippet
def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index eaa38fa6c98..62916270172 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -137,10 +137,10 @@ class ProjectsController < Projects::ApplicationController
noteable =
case params[:type]
when 'Issue'
- IssuesFinder.new(current_user, project_id: @project.id, state: 'all').
+ IssuesFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id])
when 'MergeRequest'
- MergeRequestsFinder.new(current_user, project_id: @project.id, state: 'all').
+ MergeRequestsFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id])
when 'Commit'
@project.commit(params[:type_id])
@@ -324,7 +324,12 @@ class ProjectsController < Projects::ApplicationController
end
def repo_exists?
- project.repository_exists? && !project.empty_repo?
+ project.repository_exists? && !project.empty_repo? && project.repo
+
+ rescue Gitlab::Git::Repository::NoRepository
+ project.repository.expire_exists_cache
+
+ false
end
def project_view_files?
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 61517d21f9f..d01e0dedf52 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -6,8 +6,6 @@ class SearchController < ApplicationController
layout 'search'
def show
- return if params[:search].nil? || params[:search].blank?
-
if params[:project_id].present?
@project = Project.find_by(id: params[:project_id])
@project = nil unless can?(current_user, :download_code, @project)
@@ -18,6 +16,8 @@ class SearchController < ApplicationController
@group = nil unless can?(current_user, :read_group, @group)
end
+ return if params[:search].nil? || params[:search].blank?
+
@search_term = params[:search]
@scope = params[:scope]
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 7271c933b9b..3085ff33aba 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -3,12 +3,19 @@ class SentNotificationsController < ApplicationController
def unsubscribe
@sent_notification = SentNotification.for(params[:id])
+
return render_404 unless @sent_notification && @sent_notification.unsubscribable?
+ return unsubscribe_and_redirect if current_user || params[:force]
+ end
+ private
+
+ def unsubscribe_and_redirect
noteable = @sent_notification.noteable
noteable.unsubscribe(@sent_notification.recipient)
flash[:notice] = "You have been unsubscribed from this thread."
+
if current_user
case noteable
when Issue
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 2a17c1f34db..d198782138a 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,4 +1,6 @@
class SnippetsController < ApplicationController
+ include ToggleAwardEmoji
+
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
@@ -85,6 +87,7 @@ class SnippetsController < ApplicationController
PersonalSnippet.find(params[:id])
end
end
+ alias_method :awardable, :snippet
def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index a99632454d9..838ecc837e4 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -65,7 +65,7 @@ class UsersController < ApplicationController
format.html { render 'show' }
format.json do
render json: {
- html: view_to_html_string("snippets/_snippets", collection: @snippets)
+ html: view_to_html_string("snippets/_snippets", collection: @snippets, remote: true)
}
end
end
@@ -73,7 +73,7 @@ class UsersController < ApplicationController
def calendar
calendar = contributions_calendar
- @timestamps = calendar.timestamps
+ @activity_dates = calendar.activity_dates
render 'calendar', layout: false
end
diff --git a/app/finders/access_requests_finder.rb b/app/finders/access_requests_finder.rb
new file mode 100644
index 00000000000..b6ee49df99b
--- /dev/null
+++ b/app/finders/access_requests_finder.rb
@@ -0,0 +1,27 @@
+class AccessRequestsFinder
+ attr_accessor :source
+
+ # Arguments:
+ # source - a Group or Project
+ def initialize(source)
+ @source = source
+ end
+
+ def execute(*args)
+ execute!(*args)
+ rescue Gitlab::Access::AccessDeniedError
+ []
+ end
+
+ def execute!(current_user)
+ raise Gitlab::Access::AccessDeniedError unless can_see_access_requests?(current_user)
+
+ source.requesters
+ end
+
+ private
+
+ def can_see_access_requests?(current_user)
+ source && Ability.allowed?(current_user, :"admin_#{source.class.to_s.underscore}", source)
+ end
+end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 60996b181f2..9f170428100 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -183,17 +183,12 @@ class IssuableFinder
end
def by_state(items)
- case params[:state]
- when 'closed'
- items.closed
- when 'merged'
- items.respond_to?(:merged) ? items.merged : items.closed
- when 'all'
- items
- when 'opened'
- items.opened
+ params[:state] ||= 'all'
+
+ if items.respond_to?(params[:state])
+ items.public_send(params[:state])
else
- raise 'You must specify default state'
+ items
end
end
@@ -216,7 +211,14 @@ class IssuableFinder
end
def by_search(items)
- items = items.search(search) if search
+ if search
+ items =
+ if search =~ iid_pattern
+ items.where(iid: $~[:iid])
+ else
+ items.full_search(search)
+ end
+ end
items
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index c2befa5a5b3..be00a219205 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -25,4 +25,8 @@ class IssuesFinder < IssuableFinder
def init_collection
Issue.visible_to_user(current_user)
end
+
+ def iid_pattern
+ @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
+ end
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index b258216d0d4..3b254e7d9d5 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -19,4 +19,14 @@ class MergeRequestsFinder < IssuableFinder
def klass
MergeRequest
end
+
+ private
+
+ def iid_pattern
+ @iid_pattern ||= %r{\A[
+ #{Regexp.escape(MergeRequest.reference_prefix)}
+ #{Regexp.escape(Issue.reference_prefix)}
+ ](?<iid>\d+)\z
+ }x
+ end
end
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index 641fbf838f1..32aea75486d 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -1,30 +1,34 @@
class PipelinesFinder
- attr_reader :project
+ attr_reader :project, :pipelines
def initialize(project)
@project = project
+ @pipelines = project.pipelines
end
- def execute(pipelines, scope)
- case scope
- when 'running'
- pipelines.running_or_pending
- when 'branches'
- from_ids(pipelines, ids_for_ref(pipelines, branches))
- when 'tags'
- from_ids(pipelines, ids_for_ref(pipelines, tags))
- else
- pipelines
- end
+ def execute(scope: nil)
+ scoped_pipelines =
+ case scope
+ when 'running'
+ pipelines.running_or_pending
+ when 'branches'
+ from_ids(ids_for_ref(branches))
+ when 'tags'
+ from_ids(ids_for_ref(tags))
+ else
+ pipelines
+ end
+
+ scoped_pipelines.order(id: :desc)
end
private
- def ids_for_ref(pipelines, refs)
+ def ids_for_ref(refs)
pipelines.where(ref: refs).group(:ref).select('max(id)')
end
- def from_ids(pipelines, ids)
+ def from_ids(ids)
pipelines.unscoped.where(id: ids)
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 5f3765cad0d..ebd78bf9888 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -249,7 +249,7 @@ module ApplicationHelper
milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
author_id: params[:author_id],
- issue_search: params[:issue_search],
+ search: params[:search],
label_name: params[:label_name]
}
@@ -280,32 +280,6 @@ module ApplicationHelper
end
end
- def state_filters_text_for(entity, project)
- titles = {
- opened: "Open"
- }
-
- entity_title = titles[entity] || entity.to_s.humanize
-
- count =
- if project.nil?
- nil
- elsif current_controller?(:issues)
- project.issues.visible_to_user(current_user).send(entity).count
- elsif current_controller?(:merge_requests)
- project.merge_requests.send(entity).count
- end
-
- html = content_tag :span, entity_title
-
- if count.present?
- html += " "
- html += content_tag :span, number_with_delimiter(count), class: 'badge'
- end
-
- html.html_safe
- end
-
def truncate_first_line(message, length = 50)
truncate(message.each_line.first.chomp, length: length) if message
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index aa8acbe7567..df41473543b 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -14,7 +14,8 @@ module AvatarsHelper
avatar_icon(options[:user] || options[:user_email], avatar_size),
class: "avatar has-tooltip hidden-xs s#{avatar_size}",
alt: "#{user_name}'s avatar",
- title: user_name
+ title: user_name,
+ data: { container: 'body' }
)
if options[:user]
diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb
new file mode 100644
index 00000000000..aa134cea31c
--- /dev/null
+++ b/app/helpers/award_emoji_helper.rb
@@ -0,0 +1,9 @@
+module AwardEmojiHelper
+ def toggle_award_url(awardable)
+ if @project
+ url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
+ else
+ url_for([:toggle_award_emoji, awardable])
+ end
+ end
+end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 639deb7c521..b7f48630bd4 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -56,7 +56,7 @@ module CiStatusHelper
def render_commit_status(commit, tooltip_placement: 'auto left')
project = commit.project
- path = builds_namespace_project_commit_path(project.namespace, project, commit)
+ path = pipelines_namespace_project_commit_path(project.namespace, project, commit)
render_status_with_link('commit', commit.status, path, tooltip_placement: tooltip_placement)
end
diff --git a/app/helpers/git_helper.rb b/app/helpers/git_helper.rb
index 09684955233..8ab394384f3 100644
--- a/app/helpers/git_helper.rb
+++ b/app/helpers/git_helper.rb
@@ -2,4 +2,8 @@ module GitHelper
def strip_gpg_signature(text)
text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "")
end
+
+ def short_sha(text)
+ Commit.truncate_sha(text)
+ end
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index a322a90cc4e..670a7ca36f4 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -46,6 +46,10 @@ module GitlabRoutingHelper
namespace_project_environments_path(project.namespace, project, *args)
end
+ def project_cycle_analytics_path(project, *args)
+ namespace_project_cycle_analytics_path(project.namespace, project, *args)
+ end
+
def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args)
end
@@ -66,6 +70,10 @@ module GitlabRoutingHelper
namespace_project_runner_path(@project.namespace, @project, runner, *args)
end
+ def environment_path(environment, *args)
+ namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
+ end
+
def issue_path(entity, *args)
namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
end
@@ -98,6 +106,14 @@ module GitlabRoutingHelper
end
end
+ def toggle_award_emoji_personal_snippet_path(*args)
+ toggle_award_emoji_snippet_path(*args)
+ end
+
+ def toggle_award_emoji_namespace_project_project_snippet_path(*args)
+ toggle_award_emoji_namespace_project_snippet_path(*args)
+ end
+
## Members
def project_members_url(project, *args)
namespace_project_project_members_url(project.namespace, project)
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index b9211e88473..ab880ed6de0 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -23,4 +23,29 @@ module GroupsHelper
full_title
end
end
+
+ def projects_lfs_status(group)
+ lfs_status =
+ if group.lfs_enabled?
+ group.projects.select(&:lfs_enabled?).size
+ else
+ group.projects.reject(&:lfs_enabled?).size
+ end
+
+ size = group.projects.size
+
+ if lfs_status == size
+ 'for all projects'
+ else
+ "for #{lfs_status} out of #{pluralize(size, 'project')}"
+ end
+ end
+
+ def group_lfs_status(group)
+ status = group.lfs_enabled? ? 'enabled' : 'disabled'
+
+ content_tag(:span, class: "lfs-#{status}") do
+ "#{status.humanize} #{projects_lfs_status(group)}"
+ end
+ end
end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index 109bc1a02d1..021d2b14718 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -1,4 +1,9 @@
module ImportHelper
+ def import_project_target(owner, name)
+ namespace = current_user.can_create_group? ? owner : current_user.namespace_path
+ "#{namespace}/#{name}"
+ end
+
def github_project_link(path_with_namespace)
link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank'
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index d1e432e41ae..692fadd505f 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -88,6 +88,24 @@ module IssuablesHelper
label_names.join(', ')
end
+ def issuables_state_counter_text(issuable_type, state)
+ titles = {
+ opened: "Open"
+ }
+
+ state_title = titles[state] || state.to_s.humanize
+
+ count =
+ Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do
+ issuables_count_for_state(issuable_type, state)
+ end
+
+ html = content_tag(:span, state_title)
+ html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
+
+ html.html_safe
+ end
+
private
def sidebar_gutter_collapsed?
@@ -105,4 +123,22 @@ module IssuablesHelper
issuable.open? ? :opened : :closed
end
end
+
+ def issuables_count_for_state(issuable_type, state)
+ issuables_finder = public_send("#{issuable_type}_finder")
+ issuables_finder.params[:state] = state
+
+ issuables_finder.execute.page(1).total_count
+ end
+
+ IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
+ private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
+
+ def issuables_state_counter_cache_key(issuable_type, state)
+ opts = params.with_indifferent_access
+ opts[:state] = state
+ opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
+
+ hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
+ end
end
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index 5d82abfca79..95b60aeab5f 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -1,11 +1,13 @@
module LfsHelper
+ include Gitlab::Routing.url_helpers
+
def require_lfs_enabled!
return if Gitlab.config.lfs.enabled
render(
json: {
message: 'Git LFS is not enabled on this GitLab server, contact your admin.',
- documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ documentation_url: help_url,
},
status: 501
)
@@ -25,20 +27,28 @@ module LfsHelper
def lfs_download_access?
return false unless project.lfs_enabled?
- project.public? || ci? || (user && user.can?(:download_code, project))
+ project.public? || ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
+ end
+
+ def user_can_download_code?
+ has_authentication_ability?(:download_code) && can?(user, :download_code, project)
+ end
+
+ def build_can_download_code?
+ has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
end
def lfs_upload_access?
return false unless project.lfs_enabled?
- user && user.can?(:push_code, project)
+ has_authentication_ability?(:push_code) && can?(user, :push_code, project)
end
def render_lfs_forbidden
render(
json: {
message: 'Access forbidden. Check your access level.',
- documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ documentation_url: help_url,
},
content_type: "application/vnd.git-lfs+json",
status: 403
@@ -49,7 +59,7 @@ module LfsHelper
render(
json: {
message: 'Not found.',
- documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ documentation_url: help_url,
},
content_type: "application/vnd.git-lfs+json",
status: 404
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index a9e175c3f5c..8abe7865fed 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -100,4 +100,14 @@ module MergeRequestsHelper
def merge_request_button_visibility(merge_request, closed)
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end
+
+ def merge_request_version_path(project, merge_request, merge_request_diff, start_sha = nil)
+ diffs_namespace_project_merge_request_path(
+ project.namespace, project, merge_request,
+ diff_id: merge_request_diff.id, start_sha: start_sha)
+ end
+
+ def version_index(merge_request_diff)
+ @merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff)
+ end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index e3d6fbd06ef..83a2a4ad3ec 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -35,6 +35,30 @@ module MilestonesHelper
milestone.issues.with_label(label.title).send(state).size
end
+ # Returns count of milestones for different states
+ # Uses explicit hash keys as the 'opened' state URL params differs from the db value
+ # and we need to add the total
+ def milestone_counts(milestones)
+ counts = milestones.reorder(nil).group(:state).count
+
+ {
+ opened: counts['active'] || 0,
+ closed: counts['closed'] || 0,
+ all: counts.values.sum || 0
+ }
+ end
+
+ # Show 'active' class if provided GET param matches check
+ # `or_blank` allows the function to return 'active' when given an empty param
+ # Could be refactored to be simpler but that may make it harder to read
+ def milestone_class_for_state(param, check, match_blank_param = false)
+ if match_blank_param
+ 'active' if param.blank? || param == check
+ else
+ 'active' if param == check
+ end
+ end
+
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 94c6b548ecd..e0b8dc1393b 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -1,6 +1,9 @@
module NamespacesHelper
- def namespaces_options(selected = :current_user, display_path: false)
+ def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
groups = current_user.owned_groups + current_user.masters_groups
+
+ groups << extra_group if extra_group && !Group.exists?(name: extra_group.name)
+
users = [current_user.namespace]
data_attr_group = { 'data-options-parent' => 'groups' }
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 2b0ff6c0d00..df87fac132d 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -1,21 +1,7 @@
module NavHelper
- def nav_menu_collapsed?
- cookies[:collapsed_nav] == 'true'
- end
-
- def nav_sidebar_class
- if nav_menu_collapsed?
- "sidebar-collapsed"
- else
- "sidebar-expanded"
- end
- end
-
def page_sidebar_class
if pinned_nav?
"page-sidebar-expanded page-sidebar-pinned"
- else
- "page-sidebar-collapsed"
end
end
@@ -26,7 +12,6 @@ module NavHelper
current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') ||
current_path?('merge_requests#pipelines') ||
-
current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
@@ -43,9 +28,7 @@ module NavHelper
class_name << " with-horizontal-nav" if defined?(nav) && nav
if pinned_nav?
- class_name << " header-expanded header-pinned-nav"
- else
- class_name << " header-collapsed"
+ class_name << " header-sidebar-expanded header-sidebar-pinned"
end
class_name
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index da230f76bae..b0331f36a2f 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -10,6 +10,10 @@ module NotesHelper
Ability.can_edit_note?(current_user, note)
end
+ def note_supports_slash_commands?(note)
+ Notes::SlashCommandsService.supported?(note, current_user)
+ end
+
def noteable_json(noteable)
{
id: noteable.id,
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d6efa603223..56477733ea2 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -27,7 +27,7 @@ module ProjectsHelper
author_html = ""
# Build avatar image tag
- 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]
+ author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar]
# Build name span tag
if opts[:by_username]
@@ -129,6 +129,19 @@ module ProjectsHelper
current_user.recent_push(project_ids)
end
+ def project_feature_access_select(field)
+ # Don't show option "everyone with access" if project is private
+ options = project_feature_options
+
+ if @project.private?
+ options.delete('Everyone with access')
+ highest_available_option = options.values.max if @project.project_feature.send(field) == ProjectFeature::ENABLED
+ end
+
+ options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
+ content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe
+ end
+
private
def get_project_nav_tabs(project, current_user)
@@ -422,15 +435,4 @@ module ProjectsHelper
'Everyone with access' => ProjectFeature::ENABLED
}
end
-
- def project_feature_access_select(field)
- # Don't show option "everyone with access" if project is private
- options = project_feature_options
- level = @project.project_feature.public_send(field)
-
- options.delete('Everyone with access') if @project.private? && level != ProjectFeature::ENABLED
-
- options = options_for_select(options, selected: @project.project_feature.public_send(field) || ProjectFeature::ENABLED)
- content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control").html_safe
- end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 4549c2e5bb6..8a7446b7cc7 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -7,8 +7,10 @@ module SearchHelper
projects_autocomplete(term)
].flatten
+ search_pattern = Regexp.new(Regexp.escape(term), "i")
+
generic_results = project_autocomplete + default_autocomplete + help_autocomplete
- generic_results.select! { |result| result[:label] =~ Regexp.new(term, "i") }
+ generic_results.select! { |result| result[:label] =~ search_pattern }
[
resources_results,
@@ -28,6 +30,37 @@ module SearchHelper
"Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\""
end
+ def parse_search_result(result)
+ ref = nil
+ filename = nil
+ basename = nil
+ startline = 0
+
+ result.each_line.each_with_index do |line, index|
+ if line =~ /^.*:.*:\d+:/
+ ref, filename, startline = line.split(':')
+ startline = startline.to_i - index
+ extname = Regexp.escape(File.extname(filename))
+ basename = filename.sub(/#{extname}$/, '')
+ break
+ end
+ end
+
+ data = ""
+
+ result.each_line do |line|
+ data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
+ end
+
+ OpenStruct.new(
+ filename: filename,
+ basename: basename,
+ ref: ref,
+ startline: startline,
+ data: data
+ )
+ end
+
private
# Autocomplete results for various settings pages
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 2dd0bf5d71e..3d4abf76419 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -8,7 +8,9 @@ module ServicesHelper
when "note"
"Event will be triggered when someone adds a comment"
when "issue"
- "Event will be triggered when an issue is created/updated/merged"
+ "Event will be triggered when an issue is created/updated/closed"
+ when "confidential_issue"
+ "Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request"
"Event will be triggered when a merge request is created/updated/merged"
when "build"
@@ -19,7 +21,7 @@ module ServicesHelper
end
def service_event_field_name(event)
- event = event.pluralize if %w[merge_request issue].include?(event)
+ event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events"
end
end
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
new file mode 100644
index 00000000000..d440edc55ba
--- /dev/null
+++ b/app/helpers/sidekiq_helper.rb
@@ -0,0 +1,19 @@
+module SidekiqHelper
+ SIDEKIQ_PS_REGEXP = /\A
+ (?<pid>\d+)\s+
+ (?<cpu>[\d\.,]+)\s+
+ (?<mem>[\d\.,]+)\s+
+ (?<state>[DRSTWXZNLsl\+<]+)\s+
+ (?<start>.+)\s+
+ (?<command>sidekiq.*\])\s+
+ \z/x
+
+ def parse_sidekiq_ps(line)
+ match = line.match(SIDEKIQ_PS_REGEXP)
+ if match
+ match[1..6]
+ else
+ %w[? ? ? ? ? ?]
+ end
+ end
+end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 0a5a8eb5aee..7e33a562077 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -1,10 +1,10 @@
module SnippetsHelper
- def reliable_snippet_path(snippet)
+ def reliable_snippet_path(snippet, opts = nil)
if snippet.project_id?
namespace_project_snippet_path(snippet.project.namespace,
- snippet.project, snippet)
+ snippet.project, snippet, opts)
else
- snippet_path(snippet)
+ snippet_path(snippet, opts)
end
end
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index d887cdadc34..88f374be1e5 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -34,4 +34,8 @@ module WorkhorseHelper
headers.store(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
head :ok
end
+
+ def set_workhorse_internal_api_content_type
+ headers['Content-Type'] = Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+ end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0cc709f68e4..29f1c527776 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -108,6 +108,12 @@ class Notify < BaseMailer
headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key
+ if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
+ headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>"
+
+ @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
+ end
+
if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 246477ffe88..55d2e07de08 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -146,7 +146,7 @@ class ApplicationSetting < ActiveRecord::Base
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
+ import_sources: Gitlab::ImportSources.values,
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 12cc5aaafba..ab92e820335 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -22,6 +22,18 @@ class Blob < SimpleDelegator
new(blob)
end
+ # Returns the data of the blob.
+ #
+ # If the blob is a text based blob the content is converted to UTF-8 and any
+ # invalid byte sequences are replaced.
+ def data
+ if binary?
+ super
+ else
+ @data ||= super.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
+ end
+ end
+
def no_highlighting?
size && size > 1.megabyte
end
diff --git a/app/models/board.rb b/app/models/board.rb
index 3240c4bede3..c56422914a9 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -4,4 +4,12 @@ class Board < ActiveRecord::Base
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all
validates :project, presence: true
+
+ def backlog_list
+ lists.merge(List.backlog).take
+ end
+
+ def done_list
+ lists.merge(List.done).take
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 23c8de6f650..5dbf66173de 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -1,5 +1,7 @@
module Ci
class Build < CommitStatus
+ include TokenAuthenticatable
+
belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
belongs_to :erased_by, class_name: 'User'
@@ -23,7 +25,10 @@ module Ci
acts_as_taggable
+ add_authentication_token_field :token
+
before_save :update_artifacts_size, if: :artifacts_file_changed?
+ before_save :ensure_token
before_destroy { project }
after_create :execute_hooks
@@ -38,6 +43,7 @@ module Ci
new_build.status = 'pending'
new_build.runner_id = nil
new_build.trigger_request_id = nil
+ new_build.token = nil
new_build.save
end
@@ -79,11 +85,14 @@ module Ci
after_transition any => [:success] do |build|
if build.environment.present?
- service = CreateDeploymentService.new(build.project, build.user,
- environment: build.environment,
- sha: build.sha,
- ref: build.ref,
- tag: build.tag)
+ service = CreateDeploymentService.new(
+ build.project, build.user,
+ environment: build.environment,
+ sha: build.sha,
+ ref: build.ref,
+ tag: build.tag,
+ options: build.options.to_h[:environment],
+ variables: build.variables)
service.execute(build)
end
end
@@ -148,6 +157,7 @@ module Ci
variables += runner.predefined_variables if runner
variables += project.container_registry_variables
variables += yaml_variables
+ variables += user_variables
variables += project.secret_variables
variables += trigger_request.user_variables if trigger_request
variables
@@ -172,7 +182,7 @@ module Ci
end
def repo_url
- auth = "gitlab-ci-token:#{token}@"
+ auth = "gitlab-ci-token:#{ensure_token!}@"
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
prefix + auth
end
@@ -208,29 +218,33 @@ module Ci
end
end
+ def has_trace_file?
+ File.exist?(path_to_trace) || has_old_trace_file?
+ end
+
def has_trace?
raw_trace.present?
end
def raw_trace
- if File.file?(path_to_trace)
- File.read(path_to_trace)
- elsif project.ci_id && File.file?(old_path_to_trace)
- # Temporary fix for build trace data integrity
- File.read(old_path_to_trace)
+ if File.exist?(trace_file_path)
+ File.read(trace_file_path)
else
# backward compatibility
read_attribute :trace
end
end
+ ##
+ # Deprecated
+ #
+ # This is a hotfix for CI build data integrity, see #4246
+ def has_old_trace_file?
+ project.ci_id && File.exist?(old_path_to_trace)
+ end
+
def trace
- trace = raw_trace
- if project && trace.present? && project.runners_token.present?
- trace.gsub(project.runners_token, 'xxxxxx')
- else
- trace
- end
+ hide_secrets(raw_trace)
end
def trace_length
@@ -243,6 +257,7 @@ module Ci
def trace=(trace)
recreate_trace_dir
+ trace = hide_secrets(trace)
File.write(path_to_trace, trace)
end
@@ -256,12 +271,22 @@ module Ci
def append_trace(trace_part, offset)
recreate_trace_dir
+ trace_part = hide_secrets(trace_part)
+
File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
File.open(path_to_trace, 'ab') do |f|
f.write(trace_part)
end
end
+ def trace_file_path
+ if has_old_trace_file?
+ old_path_to_trace
+ else
+ path_to_trace
+ end
+ end
+
def dir_to_trace
File.join(
Settings.gitlab_ci.builds_path,
@@ -323,12 +348,8 @@ module Ci
)
end
- def token
- project.runners_token
- end
-
def valid_token?(token)
- project.valid_runners_token?(token)
+ self.token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
def has_tags?
@@ -352,7 +373,7 @@ module Ci
end
def artifacts?
- !artifacts_expired? && self[:artifacts_file].present?
+ !artifacts_expired? && artifacts_file.exists?
end
def artifacts_metadata?
@@ -417,6 +438,15 @@ module Ci
read_attribute(:yaml_variables) || build_attributes_from_config[:yaml_variables] || []
end
+ def user_variables
+ return [] if user.blank?
+
+ [
+ { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
+ { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }
+ ]
+ end
+
private
def update_artifacts_size
@@ -452,6 +482,7 @@ module Ci
]
variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag?
variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request
+ variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if manual?
variables
end
@@ -460,5 +491,14 @@ module Ci
pipeline.config_processor.build_attributes(name)
end
+
+ def hide_secrets(trace)
+ return unless trace
+
+ trace = trace.dup
+ Ci::MaskSecret.mask!(trace, project.runners_token) if project
+ Ci::MaskSecret.mask!(trace, token)
+ trace
+ end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index bd1737c7587..663c5b1e231 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -2,6 +2,7 @@ module Ci
class Pipeline < ActiveRecord::Base
extend Ci::Model
include HasStatus
+ include Importable
self.table_name = 'ci_commits'
@@ -12,12 +13,12 @@ module Ci
has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
- validates_presence_of :sha
- validates_presence_of :ref
- validates_presence_of :status
- validate :valid_commit_sha
+ validates_presence_of :sha, unless: :importing?
+ validates_presence_of :ref, unless: :importing?
+ validates_presence_of :status, unless: :importing?
+ validate :valid_commit_sha, unless: :importing?
- after_save :keep_around_commits
+ after_save :keep_around_commits, unless: :importing?
delegate :stages, to: :statuses
@@ -55,6 +56,16 @@ module Ci
pipeline.finished_at = Time.now
end
+ after_transition [:created, :pending] => :running do |pipeline|
+ MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
+ update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
+ end
+
+ after_transition any => [:success] do |pipeline|
+ MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
+ update_all(latest_build_finished_at: pipeline.finished_at)
+ end
+
before_transition do |pipeline|
pipeline.update_duration
end
@@ -241,13 +252,16 @@ module Ci
end
def build_updated
- case latest_builds_status
- when 'pending' then enqueue
- when 'running' then run
- when 'success' then succeed
- when 'failed' then drop
- when 'canceled' then cancel
- when 'skipped' then skip
+ with_lock do
+ reload
+ case latest_builds_status
+ when 'pending' then enqueue
+ when 'running' then run
+ when 'success' then succeed
+ when 'failed' then drop
+ when 'canceled' then cancel
+ when 'skipped' then skip
+ end
end
end
@@ -257,8 +271,17 @@ module Ci
]
end
+ def queued_duration
+ return unless started_at
+
+ seconds = (started_at - created_at).to_i
+ seconds unless seconds.zero?
+ end
+
def update_duration
- self.duration = calculate_duration
+ return unless started_at
+
+ self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
end
def execute_hooks
@@ -267,6 +290,16 @@ module Ci
project.execute_services(data, :pipeline_hooks)
end
+ # Merge requests for which the current pipeline is running against
+ # the merge request's latest commit.
+ def merge_requests
+ @merge_requests ||=
+ begin
+ project.merge_requests.where(source_branch: self.ref).
+ select { |merge_request| merge_request.pipeline.try(:id) == self.id }
+ end
+ end
+
private
def pipeline_data
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 49f05f881a2..ed5d4b13b7e 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -2,7 +2,7 @@ module Ci
class Runner < ActiveRecord::Base
extend Ci::Model
- LAST_CONTACT_TIME = 5.minutes.ago
+ LAST_CONTACT_TIME = 2.hours.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index c9c47ec7419..6959223aed9 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -1,7 +1,7 @@
module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
-
+
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
validates_uniqueness_of :key, scope: :gl_project_id
@@ -11,7 +11,9 @@ module Ci
format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." }
- attr_encrypted :value,
+ scope :order_key_asc, -> { reorder(key: :asc) }
+
+ attr_encrypted :value,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
key: Gitlab::Application.secrets.db_key_base,
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 4a628924499..736db1ab0f6 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -69,11 +69,9 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now
end
- # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed
- around_transition any => [:success, :failed, :canceled] do |commit_status, block|
- block.call
-
+ after_transition any => [:success, :failed, :canceled] do |commit_status|
commit_status.pipeline.try(:process!)
+ true
end
after_transition do |commit_status, transition|
@@ -95,6 +93,10 @@ class CommitStatus < ActiveRecord::Base
pipeline.before_sha || Gitlab::Git::BLANK_SHA
end
+ def group_name
+ name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
+ end
+
def self.stages
# We group by stage name, but order stages by theirs' index
unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
@@ -113,6 +115,10 @@ class CommitStatus < ActiveRecord::Base
allow_failure? && (failed? || canceled?)
end
+ def playable?
+ false
+ end
+
def duration
calculate_duration
end
diff --git a/app/models/concerns/access_requestable.rb b/app/models/concerns/access_requestable.rb
index eedd32a729f..62bc6b809f4 100644
--- a/app/models/concerns/access_requestable.rb
+++ b/app/models/concerns/access_requestable.rb
@@ -8,9 +8,6 @@ module AccessRequestable
extend ActiveSupport::Concern
def request_access(user)
- members.create(
- access_level: Gitlab::Access::DEVELOPER,
- user: user,
- requested_at: Time.now.utc)
+ Members::RequestAccessService.new(self, user).execute
end
end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index d8d4575bb4d..073ac4c1b65 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -71,6 +71,12 @@ module Awardable
end
end
+ def user_authored?(current_user)
+ author = self.respond_to?(:author) ? self.author : self.user
+
+ author == current_user
+ end
+
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index f7b8352405c..0fa4df0fb56 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -8,8 +8,9 @@ module HasStatus
class_methods do
def status_sql
- scope = all.relevant
+ scope = all
builds = scope.select('count(*)').to_sql
+ created = scope.created.select('count(*)').to_sql
success = scope.success.select('count(*)').to_sql
ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored)
ignored ||= '0'
@@ -19,12 +20,12 @@ module HasStatus
skipped = scope.skipped.select('count(*)').to_sql
deduce_status = "(CASE
- WHEN (#{builds})=0 THEN NULL
+ WHEN (#{builds})=(#{created}) THEN 'created'
WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
- WHEN (#{builds})=(#{pending})+(#{skipped}) THEN 'pending'
+ WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending'
WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
- WHEN (#{running})+(#{pending})>0 THEN 'running'
+ WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
ELSE 'failed'
END)"
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 22231b2e0f0..ff465d2c745 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -28,10 +28,13 @@ module Issuable
loaded? && to_a.all? { |note| note.association(:award_emoji).loaded? }
end
end
+
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy
+ has_one :metrics
+
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
@@ -81,6 +84,7 @@ module Issuable
acts_as_paranoid
after_save :update_assignee_cache_counts, if: :assignee_id_changed?
+ after_save :record_metrics
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignee
@@ -196,10 +200,6 @@ module Issuable
end
end
- def user_authored?(user)
- user == author
- end
-
def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
@@ -286,4 +286,9 @@ module Issuable
def can_move?(*)
false
end
+
+ def record_metrics
+ metrics = self.metrics || create_metrics
+ metrics.record!
+ end
end
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
new file mode 100644
index 00000000000..be295487fd2
--- /dev/null
+++ b/app/models/cycle_analytics.rb
@@ -0,0 +1,97 @@
+class CycleAnalytics
+ include Gitlab::Database::Median
+ include Gitlab::Database::DateTime
+
+ def initialize(project, from:)
+ @project = project
+ @from = from
+ end
+
+ def summary
+ @summary ||= Summary.new(@project, from: @from)
+ end
+
+ def issue
+ calculate_metric(:issue,
+ Issue.arel_table[:created_at],
+ [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
+ Issue::Metrics.arel_table[:first_added_to_board_at]])
+ end
+
+ def plan
+ calculate_metric(:plan,
+ [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
+ Issue::Metrics.arel_table[:first_added_to_board_at]],
+ Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
+ end
+
+ def code
+ calculate_metric(:code,
+ Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
+ MergeRequest.arel_table[:created_at])
+ end
+
+ def test
+ calculate_metric(:test,
+ MergeRequest::Metrics.arel_table[:latest_build_started_at],
+ MergeRequest::Metrics.arel_table[:latest_build_finished_at])
+ end
+
+ def review
+ calculate_metric(:review,
+ MergeRequest.arel_table[:created_at],
+ MergeRequest::Metrics.arel_table[:merged_at])
+ end
+
+ def staging
+ calculate_metric(:staging,
+ MergeRequest::Metrics.arel_table[:merged_at],
+ MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+ end
+
+ def production
+ calculate_metric(:production,
+ Issue.arel_table[:created_at],
+ MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+ end
+
+ private
+
+ def calculate_metric(name, start_time_attrs, end_time_attrs)
+ cte_table = Arel::Table.new("cte_table_for_#{name}")
+
+ # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+ # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+ # We compute the (end_time - start_time) interval, and give it an alias based on the current
+ # cycle analytics stage.
+ interval_query = Arel::Nodes::As.new(
+ cte_table,
+ subtract_datetimes(base_query, end_time_attrs, start_time_attrs, name.to_s))
+
+ median_datetime(cte_table, interval_query, name)
+ end
+
+ # Join table with a row for every <issue,merge_request> pair (where the merge request
+ # closes the given issue) with issue and merge request metrics included. The metrics
+ # are loaded with an inner join, so issues / merge requests without metrics are
+ # automatically excluded.
+ def base_query
+ arel_table = MergeRequestsClosingIssues.arel_table
+
+ # Load issues
+ query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])).
+ join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])).
+ where(Issue.arel_table[:project_id].eq(@project.id)).
+ where(Issue.arel_table[:deleted_at].eq(nil)).
+ where(Issue.arel_table[:created_at].gteq(@from))
+
+ # Load merge_requests
+ query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin).
+ on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])).
+ join(MergeRequest::Metrics.arel_table).
+ on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id]))
+
+ # Limit to merge requests that have been deployed to production after `@from`
+ query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from))
+ end
+end
diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb
new file mode 100644
index 00000000000..b46db449bf3
--- /dev/null
+++ b/app/models/cycle_analytics/summary.rb
@@ -0,0 +1,42 @@
+class CycleAnalytics
+ class Summary
+ def initialize(project, from:)
+ @project = project
+ @from = from
+ end
+
+ def new_issues
+ @project.issues.created_after(@from).count
+ end
+
+ def commits
+ ref = @project.default_branch.presence
+ count_commits_for(ref)
+ end
+
+ def deploys
+ @project.deployments.where("created_at > ?", @from).count
+ end
+
+ private
+
+ # Don't use the `Gitlab::Git::Repository#log` method, because it enforces
+ # a limit. Since we need a commit count, we _can't_ enforce a limit, so
+ # the easiest way forward is to replicate the relevant portions of the
+ # `log` function here.
+ def count_commits_for(ref)
+ return unless ref
+
+ repository = @project.repository.raw_repository
+ sha = @project.repository.commit(ref).sha
+
+ cmd = %W(git --git-dir=#{repository.path} log)
+ cmd << '--format=%H'
+ cmd << "--after=#{@from.iso8601}"
+ cmd << sha
+
+ raw_output = IO.popen(cmd) { |io| io.read }
+ raw_output.lines.count
+ end
+ end
+end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 1e338889714..07d7e19e70d 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -42,4 +42,38 @@ class Deployment < ActiveRecord::Base
project.repository.is_ancestor?(commit.id, sha)
end
+
+ def update_merge_request_metrics!
+ return unless environment.update_merge_request_metrics?
+
+ merge_requests = project.merge_requests.
+ joins(:metrics).
+ where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }).
+ where("merge_request_metrics.merged_at <= ?", self.created_at)
+
+ if previous_deployment
+ merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at)
+ end
+
+ # Need to use `map` instead of `select` because MySQL doesn't allow `SELECT`ing from the same table
+ # that we're updating.
+ merge_request_ids =
+ if Gitlab::Database.postgresql?
+ merge_requests.select(:id)
+ elsif Gitlab::Database.mysql?
+ merge_requests.map(&:id)
+ end
+
+ MergeRequest::Metrics.
+ where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil).
+ update_all(first_deployed_to_production_at: self.created_at)
+ end
+
+ def previous_deployment
+ @previous_deployment ||=
+ project.deployments.joins(:environment).
+ where(environments: { name: self.environment.name }, ref: self.ref).
+ where.not(id: self.id).
+ take
+ end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 4442cefc7e9..559b3075905 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -13,6 +13,11 @@ class DiffNote < Note
validate :positions_complete
validate :verify_supported
+ # Keep this scope in sync with the logic in `#resolvable?`
+ scope :resolvable, -> { user.where(noteable_type: 'MergeRequest') }
+ scope :resolved, -> { resolvable.where.not(resolved_at: nil) }
+ scope :unresolved, -> { resolvable.where(resolved_at: nil) }
+
after_initialize :ensure_original_discussion_id
before_validation :set_original_position, :update_position, on: :create
before_validation :set_line_code, :set_original_discussion_id
@@ -25,6 +30,16 @@ class DiffNote < Note
def build_discussion_id(noteable_type, noteable_id, position)
[super(noteable_type, noteable_id), *position.key].join("-")
end
+
+ # This method must be kept in sync with `#resolve!`
+ def resolve!(current_user)
+ unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id)
+ end
+
+ # This method must be kept in sync with `#unresolve!`
+ def unresolve!
+ resolved.update_all(resolved_at: nil, resolved_by_id: nil)
+ end
end
def new_diff_note?
@@ -73,6 +88,7 @@ class DiffNote < Note
self.position.diff_refs == diff_refs
end
+ # If you update this method remember to also update the scope `resolvable`
def resolvable?
!system? && for_merge_request?
end
@@ -83,6 +99,7 @@ class DiffNote < Note
self.resolved_at.present?
end
+ # If you update this method remember to also update `.resolve!`
def resolve!(current_user)
return unless resolvable?
return if resolved?
@@ -92,6 +109,7 @@ class DiffNote < Note
save!
end
+ # If you update this method remember to also update `.unresolve!`
def unresolve!
return unless resolvable?
return unless resolved?
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 9676bc03470..de06c13481a 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -1,7 +1,7 @@
class Discussion
NUMBER_OF_TRUNCATED_DIFF_LINES = 16
- attr_reader :first_note, :last_note, :notes
+ attr_reader :notes
delegate :created_at,
:project,
@@ -36,8 +36,6 @@ class Discussion
end
def initialize(notes)
- @first_note = notes.first
- @last_note = notes.last
@notes = notes
end
@@ -70,17 +68,25 @@ class Discussion
end
def resolvable?
- return @resolvable if defined?(@resolvable)
+ return @resolvable if @resolvable.present?
@resolvable = diff_discussion? && notes.any?(&:resolvable?)
end
def resolved?
- return @resolved if defined?(@resolved)
+ return @resolved if @resolved.present?
@resolved = resolvable? && notes.none?(&:to_be_resolved?)
end
+ def first_note
+ @first_note ||= @notes.first
+ end
+
+ def last_note
+ @last_note ||= @notes.last
+ end
+
def resolved_notes
notes.select(&:resolved?)
end
@@ -100,17 +106,13 @@ class Discussion
def resolve!(current_user)
return unless resolvable?
- notes.each do |note|
- note.resolve!(current_user) if note.resolvable?
- end
+ update { |notes| notes.resolve!(current_user) }
end
def unresolve!
return unless resolvable?
- notes.each do |note|
- note.unresolve! if note.resolvable?
- end
+ update { |notes| notes.unresolve! }
end
def for_target?(target)
@@ -118,7 +120,7 @@ class Discussion
end
def active?
- return @active if defined?(@active)
+ return @active if @active.present?
@active = first_note.active?
end
@@ -174,4 +176,17 @@ class Discussion
prev_lines
end
+
+ private
+
+ def update
+ notes_relation = DiffNote.where(id: notes.map(&:id)).fresh
+ yield(notes_relation)
+
+ # Set the notes array to the updated notes
+ @notes = notes_relation.to_a
+
+ # Reset the memoized values
+ @last_resolved_note = @resolvable = @resolved = @first_note = @last_note = nil
+ end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 75e6f869786..49e0a20640c 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -4,6 +4,7 @@ class Environment < ActiveRecord::Base
has_many :deployments
before_validation :nullify_external_url
+ before_save :set_environment_type
validates :name,
presence: true,
@@ -26,9 +27,24 @@ class Environment < ActiveRecord::Base
self.external_url = nil if self.external_url.blank?
end
+ def set_environment_type
+ names = name.split('/')
+
+ self.environment_type =
+ if names.many?
+ names.first
+ else
+ nil
+ end
+ end
+
def includes_commit?(commit)
return false unless last_deployment
last_deployment.includes_commit?(commit)
end
+
+ def update_merge_request_metrics?
+ self.name == "production"
+ end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index a0b7b0dc2b5..55a76e26f3c 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -13,6 +13,8 @@ class Event < ActiveRecord::Base
LEFT = 9 # User left project
DESTROYED = 10
+ RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
+
delegate :name, :email, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true
@@ -324,8 +326,27 @@ class Event < ActiveRecord::Base
end
def reset_project_activity
- if project && Gitlab::ExclusiveLease.new("project:update_last_activity_at:#{project.id}", timeout: 60).try_obtain
- project.update_column(:last_activity_at, self.created_at)
- end
+ return unless project
+
+ # Don't even bother obtaining a lock if the last update happened less than
+ # 60 minutes ago.
+ return if recent_update?
+
+ return unless try_obtain_lease
+
+ project.update_column(:last_activity_at, created_at)
+ end
+
+ private
+
+ def recent_update?
+ project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
+ end
+
+ def try_obtain_lease
+ Gitlab::ExclusiveLease.
+ new("project:update_last_activity_at:#{project.id}",
+ timeout: RESET_PROJECT_ACTIVITY_INTERVAL.to_i).
+ try_obtain
end
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index da7c265a371..bda2b5c5d5d 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -8,7 +8,8 @@ class GlobalMilestone
milestones = milestones.group_by(&:title)
milestones.map do |title, milestones|
- new(title, milestones)
+ milestones_relation = Milestone.where(id: milestones.map(&:id))
+ new(title, milestones_relation)
end
end
@@ -31,7 +32,7 @@ class GlobalMilestone
end
def projects
- @projects ||= Project.for_milestones(milestones.map(&:id))
+ @projects ||= Project.for_milestones(milestones.select(:id))
end
def state
@@ -53,19 +54,19 @@ class GlobalMilestone
end
def issues
- @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project)
+ @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels)
end
def merge_requests
- @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project)
+ @merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels)
end
def participants
- @participants ||= milestones.map(&:participants).flatten.compact.uniq
+ @participants ||= milestones.includes(:participants).map(&:participants).flatten.compact.uniq
end
def labels
- @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten)
+ @labels ||= GlobalLabel.build_collection(milestones.includes(:labels).map(&:labels).flatten)
.sort_by!(&:title)
end
diff --git a/app/models/group.rb b/app/models/group.rb
index c48869ae465..a2f88cca828 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -95,40 +95,51 @@ class Group < Namespace
end
end
- def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
- user_ids.each do |user_id|
- Member.add_user(
- self.group_members,
- user_id,
- access_level,
- current_user: current_user,
- expires_at: expires_at
- )
- end
+ def lfs_enabled?
+ return false unless Gitlab.config.lfs.enabled
+ return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
+
+ self[:lfs_enabled]
+ end
+
+ def add_users(users, access_level, current_user: nil, expires_at: nil)
+ GroupMember.add_users_to_group(
+ self,
+ users,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
def add_user(user, access_level, current_user: nil, expires_at: nil)
- add_users([user], access_level, current_user: current_user, expires_at: expires_at)
+ GroupMember.add_user(
+ self,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
def add_guest(user, current_user = nil)
- add_user(user, Gitlab::Access::GUEST, current_user: current_user)
+ add_user(user, :guest, current_user: current_user)
end
def add_reporter(user, current_user = nil)
- add_user(user, Gitlab::Access::REPORTER, current_user: current_user)
+ add_user(user, :reporter, current_user: current_user)
end
def add_developer(user, current_user = nil)
- add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user)
+ add_user(user, :developer, current_user: current_user)
end
def add_master(user, current_user = nil)
- add_user(user, Gitlab::Access::MASTER, current_user: current_user)
+ add_user(user, :master, current_user: current_user)
end
def add_owner(user, current_user = nil)
- add_user(user, Gitlab::Access::OWNER, current_user: current_user)
+ add_user(user, :owner, current_user: current_user)
end
def has_owner?(user)
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 836a75b0608..c631e7a7df5 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -2,6 +2,7 @@ class ProjectHook < WebHook
belongs_to :project
scope :issue_hooks, -> { where(issues_events: true) }
+ scope :confidential_issue_hooks, -> { where(confidential_issues_events: true) }
scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
scope :build_hooks, -> { where(build_events: true) }
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index f365dee3141..595602e80fe 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -4,6 +4,7 @@ class WebHook < ActiveRecord::Base
default_value_for :push_events, true
default_value_for :issues_events, false
+ default_value_for :confidential_issues_events, false
default_value_for :note_events, false
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 788611305fe..abd58e0454a 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -23,6 +23,8 @@ class Issue < ActiveRecord::Base
has_many :events, as: :target, dependent: :destroy
+ has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
+
validates :project, presence: true
scope :cared, ->(user) { where(assignee_id: user) }
@@ -36,6 +38,8 @@ class Issue < ActiveRecord::Base
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
+ scope :created_after, -> (datetime) { where("created_at >= ?", datetime) }
+
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
diff --git a/app/models/issue/metrics.rb b/app/models/issue/metrics.rb
new file mode 100644
index 00000000000..012d545c440
--- /dev/null
+++ b/app/models/issue/metrics.rb
@@ -0,0 +1,21 @@
+class Issue::Metrics < ActiveRecord::Base
+ belongs_to :issue
+
+ def record!
+ if issue.milestone_id.present? && self.first_associated_with_milestone_at.blank?
+ self.first_associated_with_milestone_at = Time.now
+ end
+
+ if issue_assigned_to_list_label? && self.first_added_to_board_at.blank?
+ self.first_added_to_board_at = Time.now
+ end
+
+ self.save
+ end
+
+ private
+
+ def issue_assigned_to_list_label?
+ issue.labels.any? { |label| label.lists.present? }
+ end
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index 64e0d33fb20..38a278ea559 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -28,17 +28,34 @@ class Member < ActiveRecord::Base
allow_nil: true
}
+ # This scope encapsulates (most of) the conditions a row in the member table
+ # must satisfy if it is a valid permission. Of particular note:
+ #
+ # * Access requests must be excluded
+ # * Blocked users must be excluded
+ # * Invitations take effect immediately
+ # * expires_at is not implemented. A background worker purges expired rows
+ scope :active, -> do
+ is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
+ user_is_active = User.arel_table[:state].eq(:active)
+
+ includes(:user).references(:users)
+ .where(is_external_invite.or(user_is_active))
+ .where(requested_at: nil)
+ end
+
scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where(invite_token: nil) }
scope :request, -> { where.not(requested_at: nil) }
- scope :has_access, -> { where('access_level > 0') }
- scope :guests, -> { where(access_level: GUEST) }
- scope :reporters, -> { where(access_level: REPORTER) }
- scope :developers, -> { where(access_level: DEVELOPER) }
- scope :masters, -> { where(access_level: MASTER) }
- scope :owners, -> { where(access_level: OWNER) }
- scope :owners_and_masters, -> { where(access_level: [OWNER, MASTER]) }
+ scope :has_access, -> { active.where('access_level > 0') }
+
+ scope :guests, -> { active.where(access_level: GUEST) }
+ scope :reporters, -> { active.where(access_level: REPORTER) }
+ scope :developers, -> { active.where(access_level: DEVELOPER) }
+ scope :masters, -> { active.where(access_level: MASTER) }
+ scope :owners, -> { active.where(access_level: OWNER) }
+ scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) }
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
@@ -63,49 +80,70 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token)
end
- # This method is used to find users that have been entered into the "Add members" field.
- # These can be the User objects directly, their IDs, their emails, or new emails to be invited.
- def user_for_id(user_id)
- return user_id if user_id.is_a?(User)
-
- user = User.find_by(id: user_id)
- user ||= User.find_by(email: user_id)
- user ||= user_id
- user
- end
-
- def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
- user = user_for_id(user_id)
+ def add_user(source, user, access_level, current_user: nil, expires_at: nil)
+ user = retrieve_user(user)
+ access_level = retrieve_access_level(access_level)
# `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)
+ member =
+ if user.is_a?(User)
+ source.members.find_by(user_id: user.id) ||
+ source.requesters.find_by(user_id: user.id) ||
+ source.members.build(user_id: user.id)
+ else
+ source.members.build(invite_email: user)
+ end
+
+ return member unless can_update_member?(current_user, member)
+
+ member.attributes = {
+ created_by: member.created_by || current_user,
+ access_level: access_level,
+ expires_at: expires_at
+ }
+
+ if member.request?
+ ::Members::ApproveAccessRequestService.new(source, current_user, id: member.id).execute
else
- member = members.build
- member.invite_email = user
+ member.save
end
- if can_update_member?(current_user, member) || project_creator?(member, access_level)
- member.created_by ||= current_user
- member.access_level = access_level
- member.expires_at = expires_at
+ member
+ end
- member.save
- end
+ def access_levels
+ Gitlab::Access.sym_options
end
private
+ # This method is used to find users that have been entered into the "Add members" field.
+ # These can be the User objects directly, their IDs, their emails, or new emails to be invited.
+ def retrieve_user(user)
+ return user if user.is_a?(User)
+
+ User.find_by(id: user) || User.find_by(email: user) || user
+ end
+
+ def retrieve_access_level(access_level)
+ access_levels.fetch(access_level) { access_level.to_i }
+ end
+
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)
+ !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
end
- def project_creator?(member, access_level)
- member.new_record? && member.owner? &&
- access_level.to_i == ProjectMember::MASTER
+ def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil)
+ users.each do |user|
+ add_user(
+ source,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
+ end
end
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 2f13d339c89..1b54a85d064 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -12,6 +12,22 @@ class GroupMember < Member
Gitlab::Access.options_with_owner
end
+ def self.access_levels
+ Gitlab::Access.sym_options_with_owner
+ end
+
+ def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil)
+ self.transaction do
+ add_users_to_source(
+ group,
+ users,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
+ end
+ end
+
def group
source
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index ec2d40eb11c..125f26369d7 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -34,36 +34,20 @@ class ProjectMember < Member
# :master
# )
#
- def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil)
- access_level = if roles_hash.has_key?(access)
- roles_hash[access]
- elsif roles_hash.values.include?(access.to_i)
- access
- else
- raise "Non valid access"
- end
-
- users = user_ids.map { |user_id| Member.user_for_id(user_id) }
-
- ProjectMember.transaction do
+ def add_users_to_projects(project_ids, users, access_level, current_user: nil, expires_at: nil)
+ self.transaction do
project_ids.each do |project_id|
project = Project.find(project_id)
- users.each do |user|
- Member.add_user(
- project.project_members,
- user,
- access_level,
- current_user: current_user,
- expires_at: expires_at
- )
- end
+ add_users_to_source(
+ project,
+ users,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
end
-
- true
- rescue
- false
end
def truncate_teams(project_ids)
@@ -84,13 +68,15 @@ class ProjectMember < Member
truncate_teams [project.id]
end
- def roles_hash
- Gitlab::Access.sym_options
- end
-
def access_level_roles
Gitlab::Access.options
end
+
+ private
+
+ def can_update_member?(current_user, member)
+ super || (member.owner? && member.new_record?)
+ end
end
def access_field
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index b0b1313f94a..aec555dcec0 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -16,6 +16,8 @@ class MergeRequest < ActiveRecord::Base
has_many :events, as: :target, dependent: :destroy
+ has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
+
serialize :merge_params, Hash
after_create :ensure_merge_request_diff, unless: :importing?
@@ -316,6 +318,10 @@ class MergeRequest < ActiveRecord::Base
closed? && forked_source_project_missing?
end
+ def closed_without_source_project?
+ closed? && !source_project
+ end
+
def forked_source_project_missing?
return false unless for_fork?
return true unless source_project
@@ -323,6 +329,12 @@ class MergeRequest < ActiveRecord::Base
!source_project.forked_from?(target_project)
end
+ def reopenable?
+ return false if closed_without_fork? || closed_without_source_project? || merged?
+
+ closed?
+ end
+
def ensure_merge_request_diff
merge_request_diff || create_merge_request_diff
end
@@ -491,6 +503,19 @@ class MergeRequest < ActiveRecord::Base
target_project
end
+ # If the merge request closes any issues, save this information in the
+ # `MergeRequestsClosingIssues` model. This is a performance optimization.
+ # Calculating this information for a number of merge requests requires
+ # running `ReferenceExtractor` on each of them separately.
+ def cache_merge_request_closes_issues!(current_user = self.author)
+ transaction do
+ self.merge_requests_closing_issues.delete_all
+ closes_issues(current_user).each do |issue|
+ self.merge_requests_closing_issues.create!(issue: issue)
+ end
+ end
+ end
+
def closes_issue?(issue)
closes_issues.include?(issue)
end
@@ -498,7 +523,8 @@ class MergeRequest < ActiveRecord::Base
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
- messages = commits.map(&:safe_message) << description
+ messages = [description]
+ messages.concat(commits.map(&:safe_message)) if merge_request_diff
Gitlab::ClosingIssueExtractor.new(project, current_user).
closed_by_message(messages.join("\n"))
@@ -564,13 +590,11 @@ class MergeRequest < ActiveRecord::Base
end
def merge_commit_message
- message = "Merge branch '#{source_branch}' into '#{target_branch}'"
- message << "\n\n"
- message << title.to_s
- message << "\n\n"
- message << description.to_s
- message << "\n\n"
- message << "See merge request !#{iid}"
+ message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n"
+ message << "#{title}\n\n"
+ message << "#{description}\n\n" if description.present?
+ message << "See merge request #{to_reference}"
+
message
end
@@ -642,11 +666,14 @@ class MergeRequest < ActiveRecord::Base
end
def environments
- return unless diff_head_commit
+ return [] unless diff_head_commit
- target_project.environments.select do |environment|
- environment.includes_commit?(diff_head_commit)
- end
+ environments = source_project.environments_for(
+ source_branch, diff_head_commit)
+ environments += target_project.environments_for(
+ target_branch, diff_head_commit, with_tags: true)
+
+ environments.uniq
end
def state_human_name
@@ -735,10 +762,23 @@ class MergeRequest < ActiveRecord::Base
end
def all_pipelines
- @all_pipelines ||=
- if diff_head_sha && source_project
- source_project.pipelines.order(id: :desc).where(sha: commits_sha, ref: source_branch)
- end
+ return unless source_project
+
+ @all_pipelines ||= begin
+ sha = if persisted?
+ all_commits_sha
+ else
+ diff_head_sha
+ end
+
+ source_project.pipelines.order(id: :desc).
+ where(sha: sha, ref: source_branch)
+ end
+ end
+
+ # Note that this could also return SHA from now dangling commits
+ def all_commits_sha
+ merge_request_diffs.flat_map(&:commits_sha).uniq
end
def merge_commit
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
new file mode 100644
index 00000000000..99c49a020c9
--- /dev/null
+++ b/app/models/merge_request/metrics.rb
@@ -0,0 +1,11 @@
+class MergeRequest::Metrics < ActiveRecord::Base
+ belongs_to :merge_request
+
+ def record!
+ if merge_request.merged? && self.merged_at.blank?
+ self.merged_at = Time.now
+ end
+
+ self.save
+ end
+end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 445179a4487..36b8b70870b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -30,6 +30,10 @@ class MergeRequestDiff < ActiveRecord::Base
select(column_names - ['st_diffs'])
end
+ def st_commits
+ super || []
+ end
+
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
@@ -83,7 +87,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def commits
- @commits ||= load_commits(st_commits || [])
+ @commits ||= load_commits(st_commits)
end
def reload_commits
@@ -117,6 +121,14 @@ class MergeRequestDiff < ActiveRecord::Base
project.commit(head_commit_sha)
end
+ def commits_sha
+ if @commits
+ commits.map(&:sha)
+ else
+ st_commits.map { |commit| commit[:id] }
+ end
+ end
+
def diff_refs
return unless start_commit_sha || base_commit_sha
@@ -152,6 +164,10 @@ class MergeRequestDiff < ActiveRecord::Base
self == merge_request.merge_request_diff
end
+ def compare_with(sha)
+ CompareService.new.execute(project, head_commit_sha, project, sha)
+ end
+
private
def dump_commits(commits)
diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb
new file mode 100644
index 00000000000..ab597c37947
--- /dev/null
+++ b/app/models/merge_requests_closing_issues.rb
@@ -0,0 +1,7 @@
+class MergeRequestsClosingIssues < ActiveRecord::Base
+ belongs_to :merge_request
+ belongs_to :issue
+
+ validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
+ validates :issue_id, presence: true
+end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 2bd7f198030..44c3cbb2c73 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -158,7 +158,7 @@ class Milestone < ActiveRecord::Base
end
def title=(value)
- write_attribute(:title, Sanitize.clean(value.to_s)) if value.present?
+ write_attribute(:title, sanitize_title(value)) if value.present?
end
# Sorts the issues for the given IDs.
@@ -204,4 +204,8 @@ class Milestone < ActiveRecord::Base
iid
end
end
+
+ def sanitize_title(value)
+ CGI.unescape_html(Sanitize.clean(value.to_s))
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 7c29d27ce97..919b3b1f095 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -141,6 +141,11 @@ class Namespace < ActiveRecord::Base
projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id)
end
+ def lfs_enabled?
+ # User namespace will always default to the global setting
+ Gitlab.config.lfs.enabled
+ end
+
private
def repository_storage_paths
diff --git a/app/models/note.rb b/app/models/note.rb
index b94e3cff2ce..f2656df028b 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -223,10 +223,6 @@ class Note < ActiveRecord::Base
end
end
- def user_authored?(user)
- user == author
- end
-
def award_emoji?
can_be_award_emoji? && contains_emoji_only?
end
diff --git a/app/models/project.rb b/app/models/project.rb
index a6de2c48071..507228606df 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -58,7 +58,7 @@ class Project < ActiveRecord::Base
# Relations
belongs_to :creator, foreign_key: 'creator_id', class_name: 'User'
- belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
+ belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
belongs_to :namespace
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
@@ -146,6 +146,7 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
+ delegate :add_user, to: :team
# Validations
validates :creator, presence: true, on: :create
@@ -393,10 +394,9 @@ class Project < ActiveRecord::Base
end
def lfs_enabled?
- return false unless Gitlab.config.lfs.enabled
- return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
+ return namespace.lfs_enabled? if self[:lfs_enabled].nil?
- self[:lfs_enabled]
+ self[:lfs_enabled] && Gitlab.config.lfs.enabled
end
def repository_storage_path
@@ -1017,10 +1017,6 @@ class Project < ActiveRecord::Base
project_members.find_by(user_id: user)
end
- def add_user(user, access_level, current_user: nil, expires_at: nil)
- team.add_user(user, access_level, current_user: current_user, expires_at: expires_at)
- end
-
def default_branch
@default_branch ||= repository.root_ref if repository.exists?
end
@@ -1138,12 +1134,6 @@ class Project < ActiveRecord::Base
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
- # TODO (ayufan): For now we use runners_token (backward compatibility)
- # In 8.4 every build will have its own individual token valid for time of build
- def valid_build_token?(token)
- self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
- end
-
def build_coverage_enabled?
build_coverage_regex.present?
end
@@ -1288,8 +1278,40 @@ class Project < ActiveRecord::Base
end
end
+ def pushes_since_gc
+ Gitlab::Redis.with { |redis| redis.get(pushes_since_gc_redis_key).to_i }
+ end
+
+ def increment_pushes_since_gc
+ Gitlab::Redis.with { |redis| redis.incr(pushes_since_gc_redis_key) }
+ end
+
+ def reset_pushes_since_gc
+ Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
+ end
+
+ def environments_for(ref, commit, with_tags: false)
+ environment_ids = deployments.group(:environment_id).
+ select(:environment_id)
+
+ environment_ids =
+ if with_tags
+ environment_ids.where('ref=? OR tag IS TRUE', ref)
+ else
+ environment_ids.where(ref: ref)
+ end
+
+ environments.where(id: environment_ids).select do |environment|
+ environment.includes_commit?(commit)
+ end
+ end
+
private
+ def pushes_since_gc_redis_key
+ "projects/#{id}/pushes_since_gc"
+ end
+
# Prevents the creation of project_feature record for every project
def setup_project_feature
build_project_feature unless project_feature
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 9c602c582bd..8c9534c3565 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -22,6 +22,12 @@ class ProjectFeature < ActiveRecord::Base
belongs_to :project
+ default_value_for :builds_access_level, value: ENABLED, allows_nil: false
+ default_value_for :issues_access_level, value: ENABLED, allows_nil: false
+ default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false
+ default_value_for :snippets_access_level, value: ENABLED, allows_nil: false
+ default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
+
def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index 63a5ed14484..d9fba3d4a41 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -9,6 +9,10 @@ class CustomIssueTrackerService < IssueTrackerService
end
end
+ def title=(value)
+ self.properties['title'] = value if self.properties
+ end
+
def description
if self.properties && self.properties['description'].present?
self.properties['description']
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index d7c986c1a91..afebd3b6a12 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -39,7 +39,7 @@ class HipchatService < Service
end
def supported_events
- %w(push issue merge_request note tag_push build)
+ %w(push issue confidential_issue merge_request note tag_push build)
end
def execute(data)
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index abbc780dc1a..e1b937817f4 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -1,6 +1,6 @@
class SlackService < Service
prop_accessor :webhook, :username, :channel
- boolean_accessor :notify_only_broken_builds
+ boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
validates :webhook, presence: true, url: true, if: :activated?
def initialize_properties
@@ -10,6 +10,7 @@ class SlackService < Service
if properties.nil?
self.properties = {}
self.notify_only_broken_builds = true
+ self.notify_only_broken_pipelines = true
end
end
@@ -38,13 +39,15 @@ class SlackService < Service
{ type: 'text', name: 'username', placeholder: 'username' },
{ type: 'text', name: 'channel', placeholder: "#general" },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
default_fields + build_event_channels
end
def supported_events
- %w(push issue merge_request note tag_push build wiki_page)
+ %w[push issue confidential_issue merge_request note tag_push
+ build pipeline wiki_page]
end
def execute(data)
@@ -62,32 +65,22 @@ class SlackService < Service
# 'close' action. Ignore update events for now to prevent duplicate
# messages from arriving.
- message = \
- case object_kind
- when "push", "tag_push"
- PushMessage.new(data)
- when "issue"
- IssueMessage.new(data) unless is_update?(data)
- when "merge_request"
- MergeMessage.new(data) unless is_update?(data)
- when "note"
- NoteMessage.new(data)
- when "build"
- BuildMessage.new(data) if should_build_be_notified?(data)
- when "wiki_page"
- WikiPageMessage.new(data)
- end
-
- opt = {}
-
- event_channel = get_channel_field(object_kind) || channel
-
- opt[:channel] = event_channel if event_channel
- opt[:username] = username if username
+ message = get_message(object_kind, data)
if message
+ opt = {}
+
+ event_channel = get_channel_field(object_kind) || channel
+
+ opt[:channel] = event_channel if event_channel
+ opt[:username] = username if username
+
notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
+
+ true
+ else
+ false
end
end
@@ -105,6 +98,25 @@ class SlackService < Service
private
+ def get_message(object_kind, data)
+ case object_kind
+ when "push", "tag_push"
+ PushMessage.new(data)
+ when "issue"
+ IssueMessage.new(data) unless is_update?(data)
+ when "merge_request"
+ MergeMessage.new(data) unless is_update?(data)
+ when "note"
+ NoteMessage.new(data)
+ when "build"
+ BuildMessage.new(data) if should_build_be_notified?(data)
+ when "pipeline"
+ PipelineMessage.new(data) if should_pipeline_be_notified?(data)
+ when "wiki_page"
+ WikiPageMessage.new(data)
+ end
+ end
+
def get_channel_field(event)
field_name = event_channel_name(event)
self.public_send(field_name)
@@ -142,6 +154,17 @@ class SlackService < Service
false
end
end
+
+ def should_pipeline_be_notified?(data)
+ case data[:object_attributes][:status]
+ when 'success'
+ !notify_only_broken_pipelines?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
end
require "slack_service/issue_message"
@@ -149,4 +172,5 @@ require "slack_service/push_message"
require "slack_service/merge_message"
require "slack_service/note_message"
require "slack_service/build_message"
+require "slack_service/pipeline_message"
require "slack_service/wiki_page_message"
diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb
index 69c21b3fc38..0fca4267bad 100644
--- a/app/models/project_services/slack_service/build_message.rb
+++ b/app/models/project_services/slack_service/build_message.rb
@@ -9,7 +9,7 @@ class SlackService
attr_reader :user_name
attr_reader :duration
- def initialize(params, commit = true)
+ def initialize(params)
@sha = params[:sha]
@ref_type = params[:tag] ? 'tag' : 'branch'
@ref = params[:ref]
@@ -36,7 +36,7 @@ class SlackService
def message
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
- end
+ end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/slack_service/pipeline_message.rb
new file mode 100644
index 00000000000..f06b3562965
--- /dev/null
+++ b/app/models/project_services/slack_service/pipeline_message.rb
@@ -0,0 +1,79 @@
+class SlackService
+ class PipelineMessage < BaseMessage
+ attr_reader :sha, :ref_type, :ref, :status, :project_name, :project_url,
+ :user_name, :duration, :pipeline_id
+
+ def initialize(data)
+ pipeline_attributes = data[:object_attributes]
+ @sha = pipeline_attributes[:sha]
+ @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ @ref = pipeline_attributes[:ref]
+ @status = pipeline_attributes[:status]
+ @duration = pipeline_attributes[:duration]
+ @pipeline_id = pipeline_attributes[:id]
+
+ @project_name = data[:project][:path_with_namespace]
+ @project_url = data[:project][:web_url]
+ @user_name = data[:commit] && data[:commit][:author_name]
+ end
+
+ def pretext
+ ''
+ end
+
+ def fallback
+ format(message)
+ end
+
+ def attachments
+ [{ text: format(message), color: attachment_color }]
+ end
+
+ private
+
+ def message
+ "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
+ end
+
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def humanized_status
+ case status
+ when 'success'
+ 'passed'
+ else
+ status
+ end
+ end
+
+ def attachment_color
+ if status == 'success'
+ 'good'
+ else
+ 'danger'
+ end
+ end
+
+ def branch_url
+ "#{project_url}/commits/#{ref}"
+ end
+
+ def branch_link
+ "[#{ref}](#{branch_url})"
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def pipeline_url
+ "#{project_url}/pipelines/#{pipeline_id}"
+ end
+
+ def pipeline_link
+ "[#{Commit.truncate_sha(sha)}](#{pipeline_url})"
+ end
+ end
+end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index ab6ea2aae36..79d041d2775 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -33,18 +33,24 @@ class ProjectTeam
member
end
- def add_users(users, access, current_user: nil, expires_at: nil)
+ def add_users(users, access_level, current_user: nil, expires_at: nil)
ProjectMember.add_users_to_projects(
[project.id],
users,
- access,
+ access_level,
current_user: current_user,
expires_at: expires_at
)
end
- def add_user(user, access, current_user: nil, expires_at: nil)
- add_users([user], access, current_user: current_user, expires_at: expires_at)
+ def add_user(user, access_level, current_user: nil, expires_at: nil)
+ ProjectMember.add_user(
+ project,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
# Remove all users from project team
@@ -163,7 +169,7 @@ class ProjectTeam
# Each group produces a list of maximum access level per user. We take the
# max of the values produced by each group.
- if project.invited_groups.any? && project.allowed_to_share_with_group?
+ if project_shared_with_group?
project.project_group_links.each do |group_link|
invited_access = max_invited_level_for_users(group_link, user_ids)
merge_max!(access, invited_access)
@@ -200,43 +206,17 @@ class ProjectTeam
def fetch_members(level = nil)
project_members = project.members
group_members = group ? group.members : []
- invited_members = []
-
- if project.invited_groups.any? && project.allowed_to_share_with_group?
- project.project_group_links.includes(group: [:group_members]).each do |group_link|
- invited_group = group_link.group
- im = invited_group.members
-
- if level
- int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
-
- # Skip group members if we ask for masters
- # but max group access is developers
- next if int_level > group_link.group_access
-
- # If we ask for developers and max
- # group access is developers we need to provide
- # both group master, developers as devs
- if int_level == group_link.group_access
- im.where("access_level >= ?)", group_link.group_access)
- else
- im.send(level)
- end
- end
-
- invited_members << im
- end
-
- invited_members = invited_members.flatten.compact
- end
if level
- project_members = project_members.send(level)
- group_members = group_members.send(level) if group
+ project_members = project_members.public_send(level)
+ group_members = group_members.public_send(level) if group
end
user_ids = project_members.pluck(:user_id)
+
+ invited_members = fetch_invited_members(level)
user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
+
user_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids)
@@ -249,4 +229,38 @@ class ProjectTeam
def merge_max!(first_hash, second_hash)
first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new }
end
+
+ def project_shared_with_group?
+ project.invited_groups.any? && project.allowed_to_share_with_group?
+ end
+
+ def fetch_invited_members(level = nil)
+ invited_members = []
+
+ return invited_members unless project_shared_with_group?
+
+ project.project_group_links.includes(group: [:group_members]).each do |link|
+ invited_group_members = link.group.members
+
+ if level
+ numeric_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
+
+ # If we're asked for a level that's higher than the group's access,
+ # there's nothing left to do
+ next if numeric_level > link.group_access
+
+ # Make sure we include everyone _above_ the requested level as well
+ invited_group_members =
+ if numeric_level == link.group_access
+ invited_group_members.where("access_level >= ?", link.group_access)
+ else
+ invited_group_members.public_send(level)
+ end
+ end
+
+ invited_members << invited_group_members
+ end
+
+ invited_members.flatten.compact
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index f891e8374d2..51557228ab9 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -149,7 +149,7 @@ class Repository
return false unless target
GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
- rugged.branches.create(branch_name, target)
+ update_ref!(ref, target, oldrev)
end
after_create_branch
@@ -181,7 +181,7 @@ class Repository
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
- rugged.branches.delete(branch_name)
+ update_ref!(ref, newrev, oldrev)
end
after_remove_branch
@@ -215,6 +215,21 @@ class Repository
rugged.references.exist?(ref)
end
+ def update_ref!(name, newrev, oldrev)
+ # We use 'git update-ref' because libgit2/rugged currently does not
+ # offer 'compare and swap' ref updates. Without compare-and-swap we can
+ # (and have!) accidentally reset the ref to an earlier state, clobbering
+ # commits. See also https://github.com/libgit2/libgit2/issues/1534.
+ command = %w[git update-ref --stdin -z]
+ _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin|
+ stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00")
+ end
+
+ return if status.zero?
+
+ raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.")
+ end
+
# Makes sure a commit is kept around when Git garbage collection runs.
# Git GC will delete commits from the repository that are no longer in any
# branches or tags, but we want to keep some of these commits around, for
@@ -741,64 +756,61 @@ class Repository
@root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
end
- def commit_dir(user, path, message, branch)
- commit_with_hooks(user, branch) do |ref|
- committer = user_to_committer(user)
- options = {}
- options[:committer] = committer
- options[:author] = committer
-
- options[:commit] = {
- message: message,
- branch: ref,
- update_ref: false,
+ def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
+ update_branch_with_hooks(user, branch) do |ref|
+ options = {
+ commit: {
+ branch: ref,
+ message: message,
+ update_ref: false
+ }
}
+ options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
+
raw_repository.mkdir(path, options)
end
end
- 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
- options[:commit] = {
- message: message,
- branch: ref,
- update_ref: false,
+ def commit_file(user, path, content, message, branch, update, author_email: nil, author_name: nil)
+ update_branch_with_hooks(user, branch) do |ref|
+ options = {
+ commit: {
+ branch: ref,
+ message: message,
+ update_ref: false
+ },
+ file: {
+ content: content,
+ path: path,
+ update: update
+ }
}
- options[:file] = {
- content: content,
- path: path,
- update: update
- }
+ options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
Gitlab::Git::Blob.commit(raw_repository, options)
end
end
- def update_file(user, path, content, branch:, previous_path:, message:)
- commit_with_hooks(user, branch) do |ref|
- committer = user_to_committer(user)
- options = {}
- options[:committer] = committer
- options[:author] = committer
- options[:commit] = {
- message: message,
- branch: ref,
- update_ref: false
+ def update_file(user, path, content, branch:, previous_path:, message:, author_email: nil, author_name: nil)
+ update_branch_with_hooks(user, branch) do |ref|
+ options = {
+ commit: {
+ branch: ref,
+ message: message,
+ update_ref: false
+ },
+ file: {
+ content: content,
+ path: path,
+ update: true
+ }
}
- options[:file] = {
- content: content,
- path: path,
- update: true
- }
+ options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
- if previous_path
+ if previous_path && previous_path != path
options[:file][:previous_path] = previous_path
Gitlab::Git::Blob.rename(raw_repository, options)
else
@@ -807,34 +819,39 @@ class Repository
end
end
- def remove_file(user, path, message, branch)
- commit_with_hooks(user, branch) do |ref|
- committer = user_to_committer(user)
- options = {}
- options[:committer] = committer
- options[:author] = committer
- options[:commit] = {
- message: message,
- branch: ref,
- update_ref: false,
+ def remove_file(user, path, message, branch, author_email: nil, author_name: nil)
+ update_branch_with_hooks(user, branch) do |ref|
+ options = {
+ commit: {
+ branch: ref,
+ message: message,
+ update_ref: false
+ },
+ file: {
+ path: path
+ }
}
- options[:file] = {
- path: path
- }
+ options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
Gitlab::Git::Blob.remove(raw_repository, options)
end
end
- def user_to_committer(user)
+ def get_committer_and_author(user, email: nil, name: nil)
+ committer = user_to_committer(user)
+ author = Gitlab::Git::committer_hash(email: email, name: name) || committer
+
{
- email: user.email,
- name: user.name,
- time: Time.now
+ author: author,
+ committer: committer
}
end
+ def user_to_committer(user)
+ Gitlab::Git::committer_hash(email: user.email, name: user.name)
+ end
+
def can_be_merged?(source_sha, target_branch)
our_commit = rugged.branches[target_branch].target
their_commit = rugged.lookup(source_sha)
@@ -856,7 +873,7 @@ class Repository
merge_index = rugged.merge_commits(our_commit, their_commit)
return false if merge_index.conflicts?
- commit_with_hooks(user, merge_request.target_branch) do
+ update_branch_with_hooks(user, merge_request.target_branch) do
actual_options = options.merge(
parents: [our_commit, their_commit],
tree: merge_index.write_tree(rugged),
@@ -874,7 +891,7 @@ class Repository
return false unless revert_tree_id
- commit_with_hooks(user, base_branch) do
+ update_branch_with_hooks(user, base_branch) do
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
message: commit.revert_message,
@@ -891,7 +908,7 @@ class Repository
return false unless cherry_pick_tree_id
- commit_with_hooks(user, base_branch) do
+ update_branch_with_hooks(user, base_branch) do
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
message: commit.message,
@@ -907,7 +924,7 @@ class Repository
end
def resolve_conflicts(user, branch, params)
- commit_with_hooks(user, branch) do
+ update_branch_with_hooks(user, branch) do
committer = user_to_committer(user)
Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
@@ -975,54 +992,18 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
- def parse_search_result(result)
- ref = nil
- filename = nil
- basename = nil
- startline = 0
-
- result.each_line.each_with_index do |line, index|
- if line =~ /^.*:.*:\d+:/
- ref, filename, startline = line.split(':')
- startline = startline.to_i - index
- extname = Regexp.escape(File.extname(filename))
- basename = filename.sub(/#{extname}$/, '')
- break
- end
- end
-
- data = ""
-
- result.each_line do |line|
- data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
- end
-
- OpenStruct.new(
- filename: filename,
- basename: basename,
- ref: ref,
- startline: startline,
- data: data
- )
- end
-
def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
end
- def commit_with_hooks(current_user, branch)
+ def update_branch_with_hooks(current_user, branch)
update_autocrlf_option
- oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
target_branch = find_branch(branch)
was_empty = empty?
- if !was_empty && target_branch
- oldrev = target_branch.target.id
- end
-
# Make commit
newrev = yield(ref)
@@ -1030,24 +1011,19 @@ class Repository
raise CommitError.new('Failed to create commit')
end
+ if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil?
+ oldrev = Gitlab::Git::BLANK_SHA
+ else
+ oldrev = rugged.merge_base(newrev, target_branch.target.sha)
+ end
+
GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
- if was_empty || !target_branch
- # Create branch
- rugged.references.create(ref, newrev)
+ update_ref!(ref, newrev, oldrev)
+ if was_empty || !target_branch
# If repo was empty expire cache
after_create if was_empty
after_create_branch
- else
- # Update head
- current_head = find_branch(branch).target.id
-
- # Make sure target branch was not changed during pre-receive hook
- if current_head == oldrev
- rugged.references.update(ref, newrev)
- else
- raise CommitError.new('Commit was rejected because branch received new push')
- end
end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 09b4717a523..80de7175565 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -7,10 +7,12 @@ class Service < ActiveRecord::Base
default_value_for :active, false
default_value_for :push_events, true
default_value_for :issues_events, true
+ default_value_for :confidential_issues_events, true
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
default_value_for :build_events, true
+ default_value_for :pipeline_events, true
default_value_for :wiki_page_events, true
after_initialize :initialize_properties
@@ -33,6 +35,7 @@ class Service < ActiveRecord::Base
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
scope :issue_hooks, -> { where(issues_events: true, active: true) }
+ scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
@@ -100,7 +103,7 @@ class Service < ActiveRecord::Base
end
def supported_events
- %w(push tag_push issue merge_request wiki_page)
+ %w(push tag_push issue confidential_issue merge_request wiki_page)
end
def execute(data)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 5ec933601ac..8a1730f3f36 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -4,6 +4,7 @@ class Snippet < ActiveRecord::Base
include Participable
include Referable
include Sortable
+ include Awardable
default_value_for :visibility_level, Snippet::PRIVATE
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index acf36d422d1..be25c750d67 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -46,6 +46,7 @@ class ProjectPolicy < BasePolicy
can! :create_issue
can! :create_note
can! :upload_file
+ can! :read_cycle_analytics
end
def reporter_access!
@@ -64,6 +65,12 @@ class ProjectPolicy < BasePolicy
can! :read_deployment
end
+ # Permissions given when an user is team member of a project
+ def team_member_reporter_access!
+ can! :build_download_code
+ can! :build_read_container_image
+ end
+
def developer_access!
can! :admin_merge_request
can! :update_merge_request
@@ -109,6 +116,8 @@ class ProjectPolicy < BasePolicy
can! :read_commit_status
can! :read_pipeline
can! :read_container_image
+ can! :build_download_code
+ can! :build_read_container_image
end
def owner_access!
@@ -130,10 +139,11 @@ class ProjectPolicy < BasePolicy
def team_access!(user)
access = project.team.max_member_access(user.id)
- guest_access! if access >= Gitlab::Access::GUEST
- reporter_access! if access >= Gitlab::Access::REPORTER
- developer_access! if access >= Gitlab::Access::DEVELOPER
- master_access! if access >= Gitlab::Access::MASTER
+ guest_access! if access >= Gitlab::Access::GUEST
+ reporter_access! if access >= Gitlab::Access::REPORTER
+ team_member_reporter_access! if access >= Gitlab::Access::REPORTER
+ developer_access! if access >= Gitlab::Access::DEVELOPER
+ master_access! if access >= Gitlab::Access::MASTER
end
def archived_access!
@@ -195,6 +205,7 @@ class ProjectPolicy < BasePolicy
can! :read_commit_status
can! :read_container_image
can! :download_code
+ can! :read_cycle_analytics
# NOTE: may be overridden by IssuePolicy
can! :read_issue
diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb
index 5c60addbe7c..76b9f1feda7 100644
--- a/app/services/akismet_service.rb
+++ b/app/services/akismet_service.rb
@@ -29,25 +29,25 @@ class AkismetService
end
def submit_ham
- return false unless akismet_enabled?
+ submit(:ham)
+ end
- params = {
- type: 'comment',
- text: text,
- author: owner.name,
- author_email: owner.email
- }
+ def submit_spam
+ submit(:spam)
+ end
- begin
- akismet_client.submit_ham(options[:ip_address], options[:user_agent], params)
- true
- rescue => e
- Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
- false
- end
+ private
+
+ def akismet_client
+ @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
+ Gitlab.config.gitlab.url)
end
- def submit_spam
+ def akismet_enabled?
+ current_application_settings.akismet_enabled
+ end
+
+ def submit(type)
return false unless akismet_enabled?
params = {
@@ -58,22 +58,11 @@ class AkismetService
}
begin
- akismet_client.submit_spam(options[:ip_address], options[:user_agent], params)
+ akismet_client.public_send(type, options[:ip_address], options[:user_agent], params)
true
rescue => e
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
false
end
end
-
- private
-
- def akismet_client
- @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
- Gitlab.config.gitlab.url)
- end
-
- def akismet_enabled?
- current_application_settings.akismet_enabled
- end
end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 6072123b851..8ea88da8a53 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -4,11 +4,13 @@ module Auth
AUDIENCE = 'container_registry'
- def execute
- return error('not found', 404) unless registry.enabled
+ def execute(authentication_abilities:)
+ @authentication_abilities = authentication_abilities
+
+ return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
unless current_user || project
- return error('forbidden', 403) unless scope
+ return error('DENIED', status: 403, message: 'access forbidden') unless scope
end
{ token: authorized_token(scope).encoded }
@@ -74,9 +76,9 @@ module Auth
case requested_action
when 'pull'
- requested_project == project || can?(current_user, :read_container_image, requested_project)
+ requested_project.public? || build_can_pull?(requested_project) || user_can_pull?(requested_project)
when 'push'
- requested_project == project || can?(current_user, :create_container_image, requested_project)
+ build_can_push?(requested_project) || user_can_push?(requested_project)
else
false
end
@@ -85,5 +87,36 @@ module Auth
def registry
Gitlab.config.registry
end
+
+ def build_can_pull?(requested_project)
+ # Build can:
+ # 1. pull from its own project (for ex. a build)
+ # 2. read images from dependent projects if creator of build is a team member
+ @authentication_abilities.include?(:build_read_container_image) &&
+ (requested_project == project || can?(current_user, :build_read_container_image, requested_project))
+ end
+
+ def user_can_pull?(requested_project)
+ @authentication_abilities.include?(:read_container_image) &&
+ can?(current_user, :read_container_image, requested_project)
+ end
+
+ def build_can_push?(requested_project)
+ # Build can push only to the project from which it originates
+ @authentication_abilities.include?(:build_create_container_image) &&
+ requested_project == project
+ end
+
+ def user_can_push?(requested_project)
+ @authentication_abilities.include?(:create_container_image) &&
+ can?(current_user, :create_container_image, requested_project)
+ end
+
+ def error(code, status:, message: '')
+ {
+ errors: [{ code: code, message: message }],
+ http_status: status
+ }
+ end
end
end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index de48a50774e..36c93dddadb 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -31,13 +31,13 @@ module Ci
current_status = status_for_prior_stages(index)
created_builds_in_stage(index).select do |build|
- process_build(build, current_status)
+ if HasStatus::COMPLETED_STATUSES.include?(current_status)
+ process_build(build, current_status)
+ end
end
end
def process_build(build, current_status)
- return false unless HasStatus::COMPLETED_STATUSES.include?(current_status)
-
if valid_statuses_for_when(build.when).include?(current_status)
build.enqueue
true
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index ed73d8cb8c2..1c82599c579 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -16,11 +16,29 @@ module Commits
error(ex.message)
end
+ private
+
def commit
raise NotImplementedError
end
- private
+ def commit_change(action)
+ raise NotImplementedError unless repository.respond_to?(action)
+
+ into = @create_merge_request ? @commit.public_send("#{action}_branch_name") : @target_branch
+ tree_id = repository.public_send("check_#{action}_content", @commit, @target_branch)
+
+ if tree_id
+ create_target_branch(into) if @create_merge_request
+
+ repository.public_send(action, current_user, @commit, into, tree_id)
+ success
+ else
+ error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title} automatically.
+ It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content."
+ raise ChangeError, error_msg
+ end
+ end
def check_push_permissions
allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
diff --git a/app/services/commits/cherry_pick_service.rb b/app/services/commits/cherry_pick_service.rb
index f9a4efa7182..605cca36f9c 100644
--- a/app/services/commits/cherry_pick_service.rb
+++ b/app/services/commits/cherry_pick_service.rb
@@ -1,19 +1,7 @@
module Commits
class CherryPickService < ChangeService
def commit
- cherry_pick_into = @create_merge_request ? @commit.cherry_pick_branch_name : @target_branch
- cherry_pick_tree_id = repository.check_cherry_pick_content(@commit, @target_branch)
-
- if cherry_pick_tree_id
- create_target_branch(cherry_pick_into) if @create_merge_request
-
- repository.cherry_pick(current_user, @commit, cherry_pick_into, cherry_pick_tree_id)
- success
- else
- error_msg = "Sorry, we cannot cherry-pick this #{@commit.change_type_title} automatically.
- It may have already been cherry-picked, or a more recent commit may have updated some of its content."
- raise ChangeError, error_msg
- end
+ commit_change(:cherry_pick)
end
end
end
diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb
index c7de9f6f35e..addd55cb32f 100644
--- a/app/services/commits/revert_service.rb
+++ b/app/services/commits/revert_service.rb
@@ -1,19 +1,7 @@
module Commits
class RevertService < ChangeService
def commit
- revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch
- revert_tree_id = repository.check_revert_content(@commit, @target_branch)
-
- if revert_tree_id
- create_target_branch(revert_into) if @create_merge_request
-
- repository.revert(current_user, @commit, revert_into, revert_tree_id)
- success
- else
- error_msg = "Sorry, we cannot revert this #{@commit.change_type_title} automatically.
- It may have already been reverted, or a more recent commit may have updated some of its content."
- raise ChangeError, error_msg
- end
+ commit_change(:revert)
end
end
end
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index efeb9df9527..799ad3e1bd0 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -2,11 +2,9 @@ require_relative 'base_service'
class CreateDeploymentService < BaseService
def execute(deployable = nil)
- environment = project.environments.find_or_create_by(
- name: params[:environment]
- )
+ environment = find_or_create_environment
- project.deployments.create(
+ deployment = project.deployments.create(
environment: environment,
ref: params[:ref],
tag: params[:tag],
@@ -14,5 +12,43 @@ class CreateDeploymentService < BaseService
user: current_user,
deployable: deployable
)
+
+ deployment.update_merge_request_metrics!
+
+ deployment
+ end
+
+ private
+
+ def find_or_create_environment
+ project.environments.find_or_create_by(name: expanded_name) do |environment|
+ environment.external_url = expanded_url
+ end
+ end
+
+ def expanded_name
+ ExpandVariables.expand(name, variables)
+ end
+
+ def expanded_url
+ return unless url
+
+ @expanded_url ||= ExpandVariables.expand(url, variables)
+ end
+
+ def name
+ params[:environment]
+ end
+
+ def url
+ options[:url]
+ end
+
+ def options
+ params[:options] || {}
+ end
+
+ def variables
+ params[:variables] || []
end
end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index ea94818713b..e8465729d06 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -16,6 +16,8 @@ module Files
params[:file_content]
end
@last_commit_sha = params[:last_commit_sha]
+ @author_email = params[:author_email]
+ @author_name = params[:author_name]
# Validate parameters
validate
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index 6107254a34e..d00d78cee7e 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
module Files
class CreateDirService < Files::BaseService
def commit
- repository.commit_dir(current_user, @file_path, @commit_message, @target_branch)
+ repository.commit_dir(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
end
def validate
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 8eaf6db8012..bf127843d55 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
module Files
class CreateService < Files::BaseService
def commit
- repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false)
+ repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false, author_email: @author_email, author_name: @author_name)
end
def validate
diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb
index 27c881c3430..8b27ad51789 100644
--- a/app/services/files/delete_service.rb
+++ b/app/services/files/delete_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
module Files
class DeleteService < Files::BaseService
def commit
- repository.remove_file(current_user, @file_path, @commit_message, @target_branch)
+ repository.remove_file(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
end
end
end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 4fc3b640799..9e9b5b63f26 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -8,7 +8,9 @@ module Files
repository.update_file(current_user, @file_path, @file_content,
branch: @target_branch,
previous_path: @previous_path,
- message: @commit_message)
+ message: @commit_message,
+ author_email: @author_email,
+ author_name: @author_name)
end
private
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 78feb37aa2a..c499427605a 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -87,7 +87,7 @@ class GitPushService < BaseService
project.change_head(branch_name)
# Set protection on the default branch if configured
- if current_application_settings.default_branch_protection != PROTECTION_NONE
+ if current_application_settings.default_branch_protection != PROTECTION_NONE && !@project.protected_branch?(@project.default_branch)
params = {
name: @project.default_branch,
@@ -134,6 +134,7 @@ class GitPushService < BaseService
end
commit.create_cross_references!(authors[commit], closed_issues)
+ update_issue_metrics(commit, authors)
end
end
@@ -186,4 +187,11 @@ class GitPushService < BaseService
def branch_name
@branch_name ||= Gitlab::Git.ref_name(params[:ref])
end
+
+ def update_issue_metrics(commit, authors)
+ mentioned_issues = commit.all_references(authors[commit]).issues
+
+ Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil).
+ update_all(first_mentioned_in_commit_at: commit.committed_date)
+ end
end
diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb
new file mode 100644
index 00000000000..60891cbb255
--- /dev/null
+++ b/app/services/issuable/bulk_update_service.rb
@@ -0,0 +1,26 @@
+module Issuable
+ class BulkUpdateService < IssuableBaseService
+ def execute(type)
+ model_class = type.classify.constantize
+ update_class = type.classify.pluralize.constantize::UpdateService
+
+ ids = params.delete(:issuable_ids).split(",")
+ items = model_class.where(id: ids)
+
+ %i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key|
+ params.delete(key) unless params[key].present?
+ end
+
+ items.each do |issuable|
+ next unless can?(current_user, :"update_#{type}", issuable)
+
+ update_class.new(issuable.project, current_user, params).execute(issuable)
+ end
+
+ {
+ count: items.count,
+ success: !items.count.zero?
+ }
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 4c8d93999a7..57d521f2fea 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -50,6 +50,7 @@ class IssuableBaseService < BaseService
params.delete(:remove_label_ids)
params.delete(:label_ids)
params.delete(:assignee_id)
+ params.delete(:due_date)
end
end
@@ -157,6 +158,10 @@ class IssuableBaseService < BaseService
# To be overridden by subclasses
end
+ def after_update(issuable)
+ # To be overridden by subclasses
+ end
+
def update_issuable(issuable, attributes)
issuable.with_transaction_returning_status do
issuable.update(attributes.merge(updated_by: current_user))
@@ -182,6 +187,7 @@ class IssuableBaseService < BaseService
end
handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
+ after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 089b0f527e2..9ea3ce084ba 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -14,9 +14,10 @@ module Issues
end
def execute_hooks(issue, action = 'open')
- issue_data = hook_data(issue, action)
- issue.project.execute_hooks(issue_data, :issue_hooks)
- issue.project.execute_services(issue_data, :issue_hooks)
+ issue_data = hook_data(issue, action)
+ hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
+ issue.project.execute_hooks(issue_data, hooks_scope)
+ issue.project.execute_services(issue_data, hooks_scope)
end
end
end
diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb
deleted file mode 100644
index 7e19a73f71a..00000000000
--- a/app/services/issues/bulk_update_service.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Issues
- class BulkUpdateService < BaseService
- def execute
- issues_ids = params.delete(:issues_ids).split(",")
- issue_params = params
-
- %i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key|
- issue_params.delete(key) unless issue_params[key].present?
- end
-
- issues = Issue.where(id: issues_ids)
-
- issues.each do |issue|
- next unless can?(current_user, :update_issue, issue)
-
- Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue)
- end
-
- {
- count: issues.count,
- success: !issues.count.zero?
- }
- end
- end
-end
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
new file mode 100644
index 00000000000..416aee2ab51
--- /dev/null
+++ b/app/services/members/approve_access_request_service.rb
@@ -0,0 +1,31 @@
+module Members
+ class ApproveAccessRequestService < BaseService
+ include MembersHelper
+
+ attr_accessor :source
+
+ def initialize(source, current_user, params = {})
+ @source = source
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
+ access_requester = source.requesters.find_by!(condition)
+
+ raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester)
+
+ access_requester.access_level = params[:access_level] if params[:access_level]
+ access_requester.accept_request
+
+ access_requester
+ end
+
+ private
+
+ def can_update_access_requester?(access_requester)
+ access_requester && can?(current_user, action_member_permission(:update, access_requester), access_requester)
+ end
+ end
+end
diff --git a/app/services/members/request_access_service.rb b/app/services/members/request_access_service.rb
new file mode 100644
index 00000000000..2614153d900
--- /dev/null
+++ b/app/services/members/request_access_service.rb
@@ -0,0 +1,25 @@
+module Members
+ class RequestAccessService < BaseService
+ attr_accessor :source
+
+ def initialize(source, current_user)
+ @source = source
+ @current_user = current_user
+ end
+
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can_request_access?(source)
+
+ source.members.create(
+ access_level: Gitlab::Access::DEVELOPER,
+ user: current_user,
+ requested_at: Time.now.utc)
+ end
+
+ private
+
+ def can_request_access?(source)
+ source && can?(current_user, :request_access, source)
+ end
+ end
+end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 290742f1506..e57791f6818 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -83,7 +83,7 @@ module MergeRequests
closes_issue = "Closes ##{iid}"
if merge_request.description.present?
- merge_request.description += closes_issue.prepend("\n")
+ merge_request.description += closes_issue.prepend("\n\n")
else
merge_request.description = closes_issue
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 73247e62421..b0ae2dfe4ce 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -20,6 +20,7 @@ module MergeRequests
event_service.open_mr(issuable, current_user)
notification_service.new_merge_request(issuable, current_user)
todo_service.new_merge_request(issuable, current_user)
+ issuable.cache_merge_request_closes_issues!(current_user)
end
end
end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index 8437d9b8b43..e8fb1b59752 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -7,6 +7,7 @@ module MergeRequests
class PostMergeService < MergeRequests::BaseService
def execute(merge_request)
close_issues(merge_request)
+ todo_service.merge_merge_request(merge_request, current_user)
merge_request.mark_as_merged
create_merge_event(merge_request, current_user)
create_note(merge_request)
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 5cedd6f11d9..22596b4014a 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -13,6 +13,7 @@ module MergeRequests
reload_merge_requests
reset_merge_when_build_succeeds
mark_pending_todos_done
+ cache_merge_requests_closing_issues
# Leave a system note if a branch was deleted/added
if branch_added? || branch_removed?
@@ -141,6 +142,14 @@ module MergeRequests
end
end
+ # If the merge requests closes any issues, save this information in the
+ # `MergeRequestsClosingIssues` model (as a performance optimization).
+ def cache_merge_requests_closing_issues
+ @project.merge_requests.where(source_branch: @branch_name).each do |merge_request|
+ merge_request.cache_merge_request_closes_issues!(@current_user)
+ end
+ end
+
def filter_merge_requests(merge_requests)
merge_requests.uniq.select(&:source_project)
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 398ec47f0ea..f14f9e4b327 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -77,5 +77,9 @@ module MergeRequests
def close_service
MergeRequests::CloseService
end
+
+ def after_update(issuable)
+ issuable.cache_merge_request_closes_issues!(current_user)
+ end
end
end
diff --git a/app/services/milestones/create_service.rb b/app/services/milestones/create_service.rb
index 3b90399af64..b8e08c9f1eb 100644
--- a/app/services/milestones/create_service.rb
+++ b/app/services/milestones/create_service.rb
@@ -3,7 +3,7 @@ module Milestones
def execute
milestone = project.milestones.new(params)
- if milestone.save!
+ if milestone.save
event_service.open_milestone(milestone, current_user)
end
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
index 4a9a8a64653..2edbd39a9e7 100644
--- a/app/services/notes/slash_commands_service.rb
+++ b/app/services/notes/slash_commands_service.rb
@@ -5,9 +5,18 @@ module Notes
'MergeRequest' => MergeRequests::UpdateService
}
- def supported?(note)
+ def self.noteable_update_service(note)
+ UPDATE_SERVICES[note.noteable_type]
+ end
+
+ def self.supported?(note, current_user)
noteable_update_service(note) &&
- can?(current_user, :"update_#{note.noteable_type.underscore}", note.noteable)
+ current_user &&
+ current_user.can?(:"update_#{note.noteable_type.underscore}", note.noteable)
+ end
+
+ def supported?(note)
+ self.class.supported?(note, current_user)
end
def extract_commands(note)
@@ -21,13 +30,7 @@ module Notes
return if command_params.empty?
return unless supported?(note)
- noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable)
- end
-
- private
-
- def noteable_update_service(note)
- UPDATE_SERVICES[note.noteable_type]
+ self.class.noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable)
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 6139ed56e25..de8049b8e2e 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -134,7 +134,8 @@ class NotificationService
merge_request,
merge_request.target_project,
current_user,
- :merged_merge_request_email
+ :merged_merge_request_email,
+ skip_current_user: !merge_request.merge_when_build_succeeds?
)
end
@@ -514,9 +515,16 @@ class NotificationService
end
end
- def close_resource_email(target, project, current_user, method)
+ def close_resource_email(target, project, current_user, method, skip_current_user: true)
action = method == :merged_merge_request_email ? "merge" : "close"
- recipients = build_recipients(target, project, current_user, action: action)
+
+ recipients = build_recipients(
+ target,
+ project,
+ current_user,
+ action: action,
+ skip_current_user: skip_current_user
+ )
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
@@ -557,7 +565,7 @@ class NotificationService
end
end
- def build_recipients(target, project, current_user, action: nil, previous_assignee: nil)
+ def build_recipients(target, project, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
custom_action = build_custom_key(action, target)
recipients = target.participants(current_user)
@@ -586,7 +594,8 @@ class NotificationService
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
- recipients.delete(current_user)
+ recipients.delete(current_user) if skip_current_user
+
recipients.uniq
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 8a53f65aec1..a08c6fcd94b 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -27,6 +27,8 @@ module Projects
# Git data (e.g. a list of branch names).
flush_caches(project, wiki_path)
+ Projects::UnlinkForkService.new(project, current_user).execute
+
Project.transaction do
project.destroy!
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index 29b3981f49f..c3dfc8cfbe8 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -30,10 +30,8 @@ module Projects
end
def increment!
- if Gitlab::ExclusiveLease.new("project_housekeeping:increment!:#{@project.id}", timeout: 60).try_obtain
- Gitlab::Metrics.measure(:increment_pushes_since_gc) do
- update_pushes_since_gc(@project.pushes_since_gc + 1)
- end
+ Gitlab::Metrics.measure(:increment_pushes_since_gc) do
+ @project.increment_pushes_since_gc
end
end
@@ -43,14 +41,10 @@ module Projects
GitGarbageCollectWorker.perform_async(@project.id)
ensure
Gitlab::Metrics.measure(:reset_pushes_since_gc) do
- update_pushes_since_gc(0)
+ @project.reset_pushes_since_gc
end
end
- def update_pushes_since_gc(new_value)
- @project.update_column(:pushes_since_gc, new_value)
- end
-
def try_obtain_lease
Gitlab::Metrics.measure(:obtain_housekeeping_lease) do
lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index cdad0426b02..e466ffa60eb 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -44,6 +44,11 @@ module Projects
begin
gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
rescue => e
+ # Expire cache to prevent scenarios such as:
+ # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
+ # 2. Retried import, repo is broken or not imported but +exists?+ still returns true
+ project.repository.before_import if project.repository_exists?
+
raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
end
end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index 9ac1124abc1..ffcad5b3a87 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -195,7 +195,7 @@ module SlashCommands
params '<in 2 days | this Friday | December 31st>'
condition do
issuable.respond_to?(:due_date) &&
- current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :due do |due_date_param|
due_date = Chronic.parse(due_date_param).try(:to_date)
@@ -208,7 +208,7 @@ module SlashCommands
issuable.persisted? &&
issuable.respond_to?(:due_date) &&
issuable.due_date? &&
- current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :remove_due_date do
@updates[:due_date] = nil
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 0c8446e7c3d..5ccaa5275b7 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -24,6 +24,7 @@ module SystemNoteService
body = "Added #{commits_text}:\n\n"
body << existing_commit_summary(noteable, existing_commits, oldrev)
body << new_commit_summary(new_commits).join("\n")
+ body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -254,8 +255,7 @@ module SystemNoteService
#
# "Started branch `201-issue-branch-button`"
def new_issue_branch(issue, project, author, branch)
- h = Gitlab::Routing.url_helpers
- link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
+ link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
body = "Started branch [`#{branch}`](#{link})"
create_note(noteable: issue, project: project, author: author, note: body)
@@ -466,4 +466,20 @@ module SystemNoteService
def escape_html(text)
Rack::Utils.escape_html(text)
end
+
+ def url_helpers
+ @url_helpers ||= Gitlab::Routing.url_helpers
+ end
+
+ def diff_comparison_url(merge_request, project, oldrev)
+ diff_id = merge_request.merge_request_diff.id
+
+ url_helpers.diffs_namespace_project_merge_request_url(
+ project.namespace,
+ project,
+ merge_request.iid,
+ diff_id: diff_id,
+ start_sha: oldrev
+ )
+ end
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 2aab8c736d6..776530ac0a5 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -31,6 +31,14 @@ class TodoService
mark_pending_todos_as_done(issue, current_user)
end
+ # When we destroy an issue we should:
+ #
+ # * refresh the todos count cache for the current user
+ #
+ def destroy_issue(issue, current_user)
+ destroy_issuable(issue, current_user)
+ end
+
# When we reassign an issue we should:
#
# * create a pending todo for new assignee if issue is assigned
@@ -64,6 +72,14 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user)
end
+ # When we destroy a merge request we should:
+ #
+ # * refresh the todos count cache for the current user
+ #
+ def destroy_merge_request(merge_request, current_user)
+ destroy_issuable(merge_request, current_user)
+ end
+
# When we reassign a merge request we should:
#
# * creates a pending todo for new assignee if merge request is assigned
@@ -187,6 +203,10 @@ class TodoService
create_mention_todos(issuable.project, issuable, author)
end
+ def destroy_issuable(issuable, user)
+ user.update_todos_count_cache
+ end
+
def toggling_tasks?(issuable)
issuable.previous_changes.include?('description') &&
issuable.tasks? && issuable.updated_tasks.any?
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index 7a35958cc5f..4dc3b2ab9a0 100644
--- a/app/validators/namespace_validator.rb
+++ b/app/validators/namespace_validator.rb
@@ -5,7 +5,8 @@
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class NamespaceValidator < ActiveModel::EachValidator
- RESERVED = %w(
+ RESERVED = %w[
+ .well-known
admin
all
assets
@@ -31,7 +32,7 @@ class NamespaceValidator < ActiveModel::EachValidator
u
unsubscribes
users
- ).freeze
+ ].freeze
def validate_each(record, attribute, value)
unless value =~ Gitlab::Regex.namespace_regex
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index d929364fc96..0d79ca7dc52 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -49,28 +49,6 @@
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.help-block#clone-protocol-help
Allow only the selected protocols to be used for Git access.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :version_check_enabled do
- = f.check_box :version_check_enabled
- Version check enabled
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :email_author_in_body do
- = f.check_box :email_author_in_body
- Include author name in notification email body
- .help-block
- Some email servers do not support overriding the email sender name.
- Enable this option to include the name of the author of the issue,
- merge request or comment in the email body instead.
- .form-group
- = f.label :admin_notification_email, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :admin_notification_email, class: 'form-control'
- .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
@@ -341,6 +319,15 @@
%a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com
%fieldset
+ %legend Abuse reports
+ .form-group
+ = f.label :admin_notification_email, 'Abuse reports 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 Error Reporting and Logging
%p
These settings require a restart to take effect.
@@ -407,6 +394,29 @@
= succeed "." do
= link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+ %fieldset
+ %legend Usage statistics
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :version_check_enabled do
+ = f.check_box :version_check_enabled
+ Version check enabled
+ .help-block
+ Let GitLab inform you when an update is available.
+
+ %fieldset
+ %legend Email
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :email_author_in_body do
+ = f.check_box :email_author_in_body
+ Include author name in notification email body
+ .help-block
+ Some email servers do not support overriding the email sender name.
+ Enable this option to include the name of the author of the issue,
+ merge request or comment in the email body instead.
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml
index 107fc25244a..b3530915068 100644
--- a/app/views/admin/background_jobs/_head.html.haml
+++ b/app/views/admin/background_jobs/_head.html.haml
@@ -1,24 +1,25 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(controller: :system_info) do
- = link_to admin_system_info_path, title: 'System Info' do
- %span
- System Info
- = nav_link(controller: :background_jobs) do
- = link_to admin_background_jobs_path, title: 'Background Jobs' do
- %span
- Background Jobs
- = nav_link(controller: :logs) do
- = link_to admin_logs_path, title: 'Logs' do
- %span
- Logs
- = nav_link(controller: :health_check) do
- = link_to admin_health_check_path, title: 'Health Check' do
- %span
- Health Check
- = nav_link(controller: :requests_profiles) do
- = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
- %span
- Requests Profiles
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: :system_info) do
+ = link_to admin_system_info_path, title: 'System Info' do
+ %span
+ System Info
+ = nav_link(controller: :background_jobs) do
+ = link_to admin_background_jobs_path, title: 'Background Jobs' do
+ %span
+ Background Jobs
+ = nav_link(controller: :logs) do
+ = link_to admin_logs_path, title: 'Logs' do
+ %span
+ Logs
+ = nav_link(controller: :health_check) do
+ = link_to admin_health_check_path, title: 'Health Check' do
+ %span
+ Health Check
+ = nav_link(controller: :requests_profiles) do
+ = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
+ %span
+ Requests Profiles
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 4f680b507c4..05855db963a 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -28,14 +28,10 @@
%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(' ')
-
+ - parse_sidekiq_ps(process).each do |value|
+ %td= value
.clearfix
%p
%i.fa.fa-exclamation-circle
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 6b157abf842..f952d2e9aa1 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -18,11 +18,11 @@
.form-group.js-toggle-colors-container.hide
= f.label :color, "Background Color", class: 'control-label'
.col-sm-10
- = f.color_field :color, class: "form-control"
+ = f.text_field :color, class: "form-control"
.form-group.js-toggle-colors-container.hide
= f.label :font, "Font Color", class: 'control-label'
.col-sm-10
- = f.color_field :font, class: "form-control"
+ = f.text_field :font, class: "form-control"
.form-group
= f.label :starts_at, class: 'control-label'
.col-sm-10.datetime-controls
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
deleted file mode 100644
index f29d9c94441..00000000000
--- a/app/views/admin/builds/_build.html.haml
+++ /dev/null
@@ -1,77 +0,0 @@
-- project = build.project
-%tr.build.commit
- %td.status
- = ci_status_with_icon(build.status)
-
- %td
- .branch-commit
- - if can?(current_user, :read_build, build.project)
- = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
- %span.build-link ##{build.id}
- - else
- %span.build-link ##{build.id}
-
- - if build.ref
- .icon-container
- = build.tag? ? icon('tag') : icon('code-fork')
- = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
- - else
- .light none
- .icon-container
- = custom_icon("icon_commit")
-
- = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace commit-id"
- - if build.stuck?
- %i.fa.fa-warning.text-warning
-
- .label-container
- - if build.tags.any?
- - build.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if build.try(:trigger_request)
- %span.label.label-info triggered
- - if build.try(:allow_failure)
- %span.label.label-danger allowed to fail
-
- %td
- - if project
- = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project)
-
- %td
- - if build.try(:runner)
- = runner_link(build.runner)
- - else
- .light none
-
- %td
- #{build.stage} / #{build.name}
-
- %td
- - if build.duration
- %p.duration
- = custom_icon("icon_timer")
- = duration_in_numbers(build.duration)
-
- - if build.finished_at
- %p.finished-at
- = icon("calendar")
- %span #{time_ago_with_tooltip(build.finished_at)}
-
- - if defined?(coverage) && coverage
- %td.coverage
- - if build.try(:coverage)
- #{build.coverage}%
-
- %td
- .pull-right
- - if can?(current_user, :read_build, project) && build.artifacts?
- = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do
- %i.fa.fa-download
- - if can?(current_user, :update_build, build.project)
- - if build.active?
- = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
- %i.fa.fa-remove.cred
- - elsif defined?(allow_retry) && allow_retry && build.retryable?
- = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
- %i.fa.fa-refresh
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 3d77634d8fa..26a8846b609 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -4,26 +4,8 @@
%div{ class: container_class }
.top-area
- %ul.nav-links
- %li{class: ('active' if @scope.nil?)}
- = link_to admin_builds_path do
- All
- %span.badge.js-totalbuilds-count= @all_builds.count(:id)
-
- %li{class: ('active' if @scope == 'pending')}
- = link_to admin_builds_path(scope: :pending) do
- Pending
- %span.badge= number_with_delimiter(@all_builds.pending.count(:id))
-
- %li{class: ('active' if @scope == 'running')}
- = link_to admin_builds_path(scope: :running) do
- Running
- %span.badge= number_with_delimiter(@all_builds.running.count(:id))
-
- %li{class: ('active' if @scope == 'finished')}
- = link_to admin_builds_path(scope: :finished) do
- Finished
- %span.badge= number_with_delimiter(@all_builds.finished.count(:id))
+ - build_path_proc = ->(scope) { admin_builds_path(scope: scope) }
+ = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
.nav-controls
- if @all_builds.running_or_pending.any?
@@ -33,23 +15,4 @@
#{(@scope || 'all').capitalize} builds
%ul.content-list.builds-content-list
- - if @builds.blank?
- %li
- .nothing-here-block No builds to show
- - else
- .table-holder
- %table.table.builds
- %thead
- %tr
- %th Status
- %th Commit
- %th Project
- %th Runner
- %th Name
- %th
- %th
-
- - @builds.each do |build|
- = render "admin/builds/build", build: build
-
- = paginate @builds, theme: 'gitlab'
+ = render "projects/builds/table", builds: @builds, admin: true
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index c91ab4cb946..ec40391a3e3 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -1,28 +1,29 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview' do
- %span
- Overview
- = nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
- %span
- Projects
- = nav_link(controller: :users) do
- = link_to admin_users_path, title: 'Users' do
- %span
- Users
- = nav_link(controller: :groups) do
- = link_to admin_groups_path, title: 'Groups' do
- %span
- Groups
- = nav_link path: 'builds#index' do
- = link_to admin_builds_path, title: 'Builds' do
- %span
- Builds
- = nav_link path: ['runners#index', 'runners#show'] do
- = link_to admin_runners_path, title: 'Runners' do
- %span
- Runners
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
+ = link_to admin_root_path, title: 'Overview' do
+ %span
+ Overview
+ = nav_link(controller: [:admin, :projects]) do
+ = link_to admin_namespaces_projects_path, title: 'Projects' do
+ %span
+ Projects
+ = nav_link(controller: :users) do
+ = link_to admin_users_path, title: 'Users' do
+ %span
+ Users
+ = nav_link(controller: :groups) do
+ = link_to admin_groups_path, title: 'Groups' do
+ %span
+ Groups
+ = nav_link path: 'builds#index' do
+ = link_to admin_builds_path, title: 'Builds' do
+ %span
+ Builds
+ = nav_link path: ['runners#index', 'runners#show'] do
+ = link_to admin_runners_path, title: 'Runners' do
+ %span
+ Runners
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 5f7fdfdb011..817910f7ddf 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -13,6 +13,8 @@
.col-sm-offset-2.col-sm-10
= render 'shared/allow_request_access', form: f
+ = render 'groups/group_lfs_settings', f: f
+
- if @group.new_record?
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index bb374694400..0188ed448ce 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -37,6 +37,12 @@
%strong
= @group.created_at.to_s(:medium)
+ %li
+ %span.light Group Git LFS status:
+ %strong
+ = group_lfs_status(@group)
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+
.panel.panel-default
.panel-heading
%h3.panel-title
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index 602cfa9b6fc..d5e6bede36a 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -14,7 +14,7 @@
.col-sm-10
.input-group
.input-group-addon.label-color-preview &nbsp;
- = f.color_field :color, class: "form-control"
+ = f.text_field :color, class: "form-control"
.help-block
Choose any color.
%br
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index a53876d6757..b760b42fde0 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -5,8 +5,10 @@
%p.prepend-top-default
%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.
+ 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.
%br
Registration token is
%code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
@@ -24,27 +26,27 @@
.bs-callout
%p
- A 'runner' is a process which runs a build.
- You can setup as many runners as you need.
+ A 'Runner' is a process which runs a build.
+ You can setup as many Runners as you need.
%br
- Runners can be placed on separate users, servers, and even on your local machine.
+ Runners can be placed on separate users, servers, even on your local machine.
%br
%div
- %span Each runner can be in one of the following states:
+ %span Each Runner can be in one of the following states:
%ul
%li
%span.label.label-success shared
- \- run builds from all unassigned projects
+ \- Runner runs builds from all unassigned projects
%li
%span.label.label-info specific
- \- run builds from assigned projects
+ \- Runner runs builds from assigned projects
%li
%span.label.label-warning locked
- \- runner cannot be assigned to other projects
+ \- Runner cannot be assigned to other projects
%li
%span.label.label-danger paused
- \- runner will not receive any new builds
+ \- Runner will not receive any new builds
.append-bottom-20.clearfix
.pull-left
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 61abfc6ecbe..a5e82e55cc1 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -11,14 +11,14 @@
- if @runner.shared?
.bs-callout.bs-callout-success
- %h4 This runner will process builds 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.
+ 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 builds only from ASSIGNED projects
- %p You can't make this a shared runner.
+ %h4 This Runner will process builds only from ASSIGNED projects
+ %p You can't make this a shared Runner.
%hr
.append-bottom-20
@@ -26,7 +26,7 @@
.row
.col-md-6
- %h4 Restrict projects for this runner
+ %h4 Restrict projects for this Runner
- if @runner.projects.any?
%table.table.assigned-projects
%thead
@@ -70,7 +70,7 @@
= paginate @projects
.col-md-6
- %h4 Recent builds served by this runner
+ %h4 Recent builds served by this Runner
%table.table.builds.runner-builds
%thead
%tr
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 02efcecc889..fbe3ab912b6 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -1,5 +1,5 @@
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
-.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } }
+.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji, sprite: false)
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index f7875e68b7e..d5c21c6dffe 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -21,13 +21,16 @@
%br
%b Tag list:
- = build[:tags]
+ = build[:tag_list].to_a.join(", ")
%br
%b Refs only:
- = build[:only] && build[:only].join(", ")
+ = @jobs[build[:name].to_sym][:only].to_a.join(", ")
%br
%b Refs except:
- = build[:except] && build[:except].join(", ")
+ = @jobs[build[:name].to_sym][:except].to_a.join(", ")
+ %br
+ %b Environment:
+ = build[:environment]
%br
%b When:
= build[:when]
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index d4e7862981c..b2af438ea57 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -4,10 +4,10 @@
= render 'dashboard/snippets_head'
.nav-block
- .controls
- = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
+ .controls.hidden-xs
+ = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
= icon('plus')
- New Snippet
+ New snippet
.nav-links.snippet-scope-menu
%li{ class: ("active" unless params[:scope]) }
@@ -34,5 +34,9 @@
%span.badge
= current_user.snippets.are_public.count
-= render 'snippets/snippets'
+ .visible-xs
+ = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
+ = icon('plus')
+ New snippet
+= render 'snippets/snippets'
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 689cd6ed665..2ef383960f4 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -1,4 +1,4 @@
-= form_tag(user_omniauth_callback_path(server['provider_name']), id: 'new_ldap_user' ) do
+= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user') do
= text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"}
= password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
- if devise_mapping.rememberable?
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 4debd3d608f..e623f7cff88 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -18,6 +18,5 @@
= f.submit "Verify code", class: "btn btn-save"
- if @user.two_factor_u2f_enabled?
-
%hr
- = render "u2f/authenticate"
+ = render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name }
diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml
index 69bd416c4de..7ed09dd1a98 100644
--- a/app/views/discussions/_jump_to_next.html.haml
+++ b/app/views/discussions/_jump_to_next.html.haml
@@ -5,5 +5,5 @@
%button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
title: "Jump to next unresolved discussion",
"aria-label" => "Jump to next unresolved discussion",
- data: { container: "body" } }
+ data: { container: "body" }}
= custom_icon("next_discussion")
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index fbe470bed2c..dfdbdf1f969 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -10,6 +10,7 @@
.btn-group{ role: "group" }
= link_to_reply_discussion(discussion, line_type)
= render "discussions/resolve_all", discussion: discussion
- = render "discussions/jump_to_next", discussion: discussion
+ - if discussion.for_merge_request?
+ = render "discussions/jump_to_next", discussion: discussion
- else
= link_to_reply_discussion(discussion)
diff --git a/app/views/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml
index 7a8767ddba0..f0b61e0f7de 100644
--- a/app/views/discussions/_resolve_all.html.haml
+++ b/app/views/discussions/_resolve_all.html.haml
@@ -1,11 +1,10 @@
- if discussion.for_merge_request?
- %resolve-discussion-btn{ ":namespace-path" => "'#{discussion.project.namespace.path}'",
- ":project-path" => "'#{discussion.project.path}'",
+ %resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'",
":discussion-id" => "'#{discussion.id}'",
":merge-request-id" => discussion.noteable.iid,
":can-resolve" => discussion.can_resolve?(current_user),
"inline-template" => true }
.btn-group{ role: "group", "v-if" => "showButton" }
- %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading" }
+ %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading", "v-cloak" => "true" }
= icon("spinner spin", "v-show" => "loading")
{{ buttonText }}
diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
index bfa95ce79a7..9f02a8d2ed9 100644
--- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
@@ -6,4 +6,4 @@
= form_tag path do
%input{:name => "_method", :type => "hidden", :value => "delete"}/
- = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-sm'
+ = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-remove btn-sm'
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index cd485da5104..132bbe26fe0 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -8,7 +8,7 @@
- else
Any
%b.caret
- %ul.dropdown-menu
+ %ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(visibility_level: nil) do
Any
@@ -28,7 +28,7 @@
- else
Any
%b.caret
- %ul.dropdown-menu
+ %ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(tag: nil) do
Any
diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml
index 6306fe6d0bf..7def9eacdc9 100644
--- a/app/views/explore/snippets/index.html.haml
+++ b/app/views/explore/snippets/index.html.haml
@@ -8,9 +8,8 @@
.row-content-block
- if current_user
- .pull-right
- = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
- New Snippet
+ = link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
+ New snippet
.oneline
Public snippets created by you and other users are listed here
diff --git a/app/views/groups/_group_lfs_settings.html.haml b/app/views/groups/_group_lfs_settings.html.haml
new file mode 100644
index 00000000000..af57065f0fc
--- /dev/null
+++ b/app/views/groups/_group_lfs_settings.html.haml
@@ -0,0 +1,11 @@
+- if current_user.admin?
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :lfs_enabled do
+ = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
+ %strong
+ Allow projects within this group to use Git LFS
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ %br/
+ %span.descr This setting can be overridden in each project. \ No newline at end of file
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index decb89b2fd6..c766370d5a0 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -25,6 +25,8 @@
.col-sm-offset-2.col-sm-10
= render 'shared/allow_request_access', form: f
+ = render 'group_lfs_settings', f: f
+
.form-group
%hr
= f.label :share_with_group_lock, class: 'control-label' do
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index 742f9d7a433..3be7ed8432c 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,3 +1,3 @@
:plain
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
- new MemberExpirationDate();
+ new gl.MemberExpirationDate();
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 53ed4fa991d..31db6ee0cad 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -23,7 +23,7 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
-%div{ class: container_class }
+%div.groups-header{ class: container_class }
.top-area
%ul.nav-links
%li.active
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index ce4536ebdc6..65842a0479b 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -7,277 +7,284 @@
Keyboard Shortcuts
%small
= link_to '(Show all)', '#', class: 'js-more-help-button'
- .modal-body.shortcuts-cheatsheet
- .col-lg-4
- %table.shortcut-mappings
- %tbody
- %tr
- %th
- %th Global Shortcuts
- %tr
- %td.shortcut
- .key s
- %td Focus Search
- %tr
- %td.shortcut
- .key f
- %td Focus Filter
- %tr
- %td.shortcut
- .key ?
- %td Show/hide this dialog
- %tr
- %td.shortcut
- - if browser.platform.mac?
- .key &#8984; shift p
- - else
- .key ctrl shift p
- %td Toggle Markdown preview
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-up
- %td Edit last comment (when focused on an empty textarea)
- %tbody
- %tr
- %th
- %th Project Files browsing
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-up
- %td Move selection up
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-down
- %td Move selection down
- %tr
- %td.shortcut
- .key enter
- %td Open Selection
- %tbody
- %tr
- %th
- %th Finding Project File
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-up
- %td Move selection up
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-down
- %td Move selection down
- %tr
- %td.shortcut
- .key enter
- %td Open Selection
- %tr
- %td.shortcut
- .key esc
- %td Go back
+ .modal-body
+ .row
+ .col-lg-4
+ %table.shortcut-mappings
+ %tbody
+ %tr
+ %th
+ %th Global Shortcuts
+ %tr
+ %td.shortcut
+ .key s
+ %td Focus Search
+ %tr
+ %td.shortcut
+ .key f
+ %td Focus Filter
+ %tr
+ %td.shortcut
+ .key ?
+ %td Show/hide this dialog
+ %tr
+ %td.shortcut
+ - if browser.platform.mac?
+ .key &#8984; shift p
+ - else
+ .key ctrl shift p
+ %td Toggle Markdown preview
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ %td Edit last comment (when focused on an empty textarea)
+ %tbody
+ %tr
+ %th
+ %th Project Files browsing
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ %td Move selection up
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-down
+ %td Move selection down
+ %tr
+ %td.shortcut
+ .key enter
+ %td Open Selection
+ %tbody
+ %tr
+ %th
+ %th Finding Project File
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ %td Move selection up
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-down
+ %td Move selection down
+ %tr
+ %td.shortcut
+ .key enter
+ %td Open Selection
+ %tr
+ %td.shortcut
+ .key esc
+ %td Go back
- .col-lg-4
- %table.shortcut-mappings
- %tbody{ class: 'hidden-shortcut project', style: 'display:none' }
- %tr
- %th
- %th Global Dashboard
- %tr
- %td.shortcut
- .key g
- .key a
- %td
- Go to the activity feed
- %tr
- %td.shortcut
- .key g
- .key p
- %td
- Go to projects
- %tr
- %td.shortcut
- .key g
- .key i
- %td
- Go to issues
- %tr
- %td.shortcut
- .key g
- .key m
- %td
- Go to merge requests
- %tbody
- %tr
- %th
- %th Project
- %tr
- %td.shortcut
- .key g
- .key p
- %td
- Go to the project's home page
- %tr
- %td.shortcut
- .key g
- .key e
- %td
- Go to the project's activity feed
- %tr
- %td.shortcut
- .key g
- .key f
- %td
- Go to files
- %tr
- %td.shortcut
- .key g
- .key c
- %td
- Go to commits
- %tr
- %td.shortcut
- .key g
- .key b
- %td
- Go to builds
- %tr
- %td.shortcut
- .key g
- .key n
- %td
- Go to network graph
- %tr
- %td.shortcut
- .key g
- .key g
- %td
- Go to graphs
- %tr
- %td.shortcut
- .key g
- .key i
- %td
- Go to issues
- %tr
- %td.shortcut
- .key g
- .key m
- %td
- Go to merge requests
- %tr
- %td.shortcut
- .key g
- .key s
- %td
- Go to snippets
- %tr
- %td.shortcut
- .key t
- %td Go to finding file
- %tr
- %td.shortcut
- .key i
- %td New issue
- .col-lg-4
- %table.shortcut-mappings
- %tbody{ class: 'hidden-shortcut network', style: 'display:none' }
- %tr
- %th
- %th Network Graph
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-left
- \/
- .key h
- %td Scroll left
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-right
- \/
- .key l
- %td Scroll right
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-up
- \/
- .key k
- %td Scroll up
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-down
- \/
- .key j
- %td Scroll down
- %tr
- %td.shortcut
- .key
- shift
- %i.fa.fa-arrow-up
- \/
- .key
- shift k
- %td Scroll to top
- %tr
- %td.shortcut
- .key
- shift
- %i.fa.fa-arrow-down
- \/
- .key
- shift j
- %td Scroll to bottom
- %tbody{ class: 'hidden-shortcut issues', style: 'display:none' }
- %tr
- %th
- %th Issues
- %tr
- %td.shortcut
- .key a
- %td Change assignee
- %tr
- %td.shortcut
- .key m
- %td Change milestone
- %tr
- %td.shortcut
- .key r
- %td Reply (quoting selected text)
- %tr
- %td.shortcut
- .key e
- %td Edit issue
- %tr
- %td.shortcut
- .key l
- %td Change Label
- %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
- %tr
- %th
- %th Merge Requests
- %tr
- %td.shortcut
- .key a
- %td Change assignee
- %tr
- %td.shortcut
- .key m
- %td Change milestone
- %tr
- %td.shortcut
- .key r
- %td Reply (quoting selected text)
- %tr
- %td.shortcut
- .key e
- %td Edit merge request
- %tr
- %td.shortcut
- .key l
- %td Change Label
+ .col-lg-4
+ %table.shortcut-mappings
+ %tbody{ class: 'hidden-shortcut project', style: 'display:none' }
+ %tr
+ %th
+ %th Global Dashboard
+ %tr
+ %td.shortcut
+ .key g
+ .key a
+ %td
+ Go to the activity feed
+ %tr
+ %td.shortcut
+ .key g
+ .key p
+ %td
+ Go to projects
+ %tr
+ %td.shortcut
+ .key g
+ .key i
+ %td
+ Go to issues
+ %tr
+ %td.shortcut
+ .key g
+ .key m
+ %td
+ Go to merge requests
+ %tbody
+ %tr
+ %th
+ %th Project
+ %tr
+ %td.shortcut
+ .key g
+ .key p
+ %td
+ Go to the project's home page
+ %tr
+ %td.shortcut
+ .key g
+ .key e
+ %td
+ Go to the project's activity feed
+ %tr
+ %td.shortcut
+ .key g
+ .key f
+ %td
+ Go to files
+ %tr
+ %td.shortcut
+ .key g
+ .key c
+ %td
+ Go to commits
+ %tr
+ %td.shortcut
+ .key g
+ .key b
+ %td
+ Go to builds
+ %tr
+ %td.shortcut
+ .key g
+ .key n
+ %td
+ Go to network graph
+ %tr
+ %td.shortcut
+ .key g
+ .key g
+ %td
+ Go to graphs
+ %tr
+ %td.shortcut
+ .key g
+ .key i
+ %td
+ Go to issues
+ %tr
+ %td.shortcut
+ .key g
+ .key l
+ %td
+ Go to issue boards
+ %tr
+ %td.shortcut
+ .key g
+ .key m
+ %td
+ Go to merge requests
+ %tr
+ %td.shortcut
+ .key g
+ .key s
+ %td
+ Go to snippets
+ %tr
+ %td.shortcut
+ .key t
+ %td Go to finding file
+ %tr
+ %td.shortcut
+ .key i
+ %td New issue
+ .col-lg-4
+ %table.shortcut-mappings
+ %tbody{ class: 'hidden-shortcut network', style: 'display:none' }
+ %tr
+ %th
+ %th Network Graph
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-left
+ \/
+ .key h
+ %td Scroll left
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-right
+ \/
+ .key l
+ %td Scroll right
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ \/
+ .key k
+ %td Scroll up
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-down
+ \/
+ .key j
+ %td Scroll down
+ %tr
+ %td.shortcut
+ .key
+ shift
+ %i.fa.fa-arrow-up
+ \/
+ .key
+ shift k
+ %td Scroll to top
+ %tr
+ %td.shortcut
+ .key
+ shift
+ %i.fa.fa-arrow-down
+ \/
+ .key
+ shift j
+ %td Scroll to bottom
+ %tbody{ class: 'hidden-shortcut issues', style: 'display:none' }
+ %tr
+ %th
+ %th Issues
+ %tr
+ %td.shortcut
+ .key a
+ %td Change assignee
+ %tr
+ %td.shortcut
+ .key m
+ %td Change milestone
+ %tr
+ %td.shortcut
+ .key r
+ %td Reply (quoting selected text)
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit issue
+ %tr
+ %td.shortcut
+ .key l
+ %td Change Label
+ %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
+ %tr
+ %th
+ %th Merge Requests
+ %tr
+ %td.shortcut
+ .key a
+ %td Change assignee
+ %tr
+ %td.shortcut
+ .key m
+ %td Change milestone
+ %tr
+ %td.shortcut
+ .key r
+ %td Reply (quoting selected text)
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit merge request
+ %tr
+ %td.shortcut
+ .key l
+ %td Change Label
diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml
index 804ad88468f..8e929538351 100644
--- a/app/views/import/base/create.js.haml
+++ b/app/views/import/base/create.js.haml
@@ -1,23 +1,4 @@
-- if @already_been_taken
- :plain
- tr = $("tr#repo_#{@repo_id}")
- target_field = tr.find(".import-target")
- import_button = tr.find(".btn-import")
- origin_target = target_field.text()
- project_name = "#{@project_name}"
- origin_namespace = "#{@target_namespace}"
- target_field.empty()
- target_field.append("<p class='alert alert-danger'>This namespace already been taken! Please choose another one</p>")
- target_field.append("<input type='text' name='target_namespace' />")
- target_field.append("/" + project_name)
- target_field.data("project_name", project_name)
- target_field.find('input').prop("value", origin_namespace)
- import_button.enable().removeClass('is-loading')
-- elsif @access_denied
- :plain
- job = $("tr#repo_#{@repo_id}")
- job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>")
-- elsif @project.persisted?
+- if @project.persisted?
:plain
job = $("tr#repo_#{@repo_id}")
job.attr("id", "project_#{@project.id}")
diff --git a/app/views/import/base/unauthorized.js.haml b/app/views/import/base/unauthorized.js.haml
new file mode 100644
index 00000000000..36f8069c1f7
--- /dev/null
+++ b/app/views/import/base/unauthorized.js.haml
@@ -0,0 +1,14 @@
+:plain
+ tr = $("tr#repo_#{@repo_id}")
+ target_field = tr.find(".import-target")
+ import_button = tr.find(".btn-import")
+ origin_target = target_field.text()
+ project_name = "#{@project_name}"
+ origin_namespace = "#{@target_namespace.path}"
+ target_field.empty()
+ target_field.append("<p class='alert alert-danger'>This namespace has already been taken! Please choose another one.</p>")
+ target_field.append("<input type='text' name='target_namespace' />")
+ target_field.append("/" + project_name)
+ target_field.data("project_name", project_name)
+ target_field.find('input').prop("value", origin_namespace)
+ import_button.enable().removeClass('is-loading')
diff --git a/app/views/import/bitbucket/deploy_key.js.haml b/app/views/import/bitbucket/deploy_key.js.haml
new file mode 100644
index 00000000000..81b34ab5c9d
--- /dev/null
+++ b/app/views/import/bitbucket/deploy_key.js.haml
@@ -0,0 +1,3 @@
+:plain
+ job = $("tr#repo_#{@repo_id}")
+ job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>")
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 15dd98077c8..f8b4b107513 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -51,7 +51,7 @@
%td
= link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
%td.import-target
- = "#{repo["owner"]}/#{repo["slug"]}"
+ = import_project_target(repo['owner'], repo['slug'])
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 54ff1d27c67..4c721d40b55 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -45,7 +45,17 @@
%td
= github_project_link(repo.full_name)
%td.import-target
- = repo.full_name
+ %fieldset.row
+ .input-group
+ .project-path.input-group-btn
+ - if current_user.can_select_namespace?
+ - selected = params[:namespace_id] || :current_user
+ - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
+ = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
+ - else
+ = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
+ %span.input-group-addon /
+ = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index fcfc6fd37f4..d31fc2e6adb 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -45,7 +45,7 @@
%td
= link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
%td.import-target
- = repo["path_with_namespace"]
+ = import_project_target(repo['namespace']['path'], repo['name'])
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index 3612f1ce5c6..baa8036de10 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,8 +1,10 @@
.flash-container.flash-container-page
- if alert
.flash-alert
- = alert
+ %div{ class: (container_class) }
+ %span= alert
- elsif notice
.flash-notice
- = notice
+ %div{ class: (container_class) }
+ %span= notice
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index bf50633af24..3950b4f53e7 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,5 +1,5 @@
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
- .sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
+ .sidebar-wrapper.nicescroll
.sidebar-action-buttons
= link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do
%span.sr-only Toggle navigation
@@ -20,6 +20,7 @@
.container-fluid
= render "layouts/nav/#{nav}"
.content-wrapper{ class: "#{layout_nav_class}" }
+ = yield :sub_nav
= render "layouts/broadcast"
= render "layouts/flash"
= yield :flash_message
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index f7580f00159..d7386105b7d 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -2,15 +2,18 @@
- label = 'This group'
- if controller.controller_path =~ /^projects/ && @project.persisted?
- label = 'This project'
-
+- if @group && @group.persisted? && @group.path
+ - group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }
+- if @project && @project.persisted?
+ - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: namespace_project_issues_path(@project.namespace, @project), mr_path: namespace_project_merge_requests_path(@project.namespace, @project) }
.search.search-form{class: "#{'has-location-badge' if label.present?}"}
= form_tag search_path, method: :get, class: 'navbar-form' do |f|
.search-input-container
- if label.present?
.location-badge= label
.search-input-wrap
- .dropdown{ data: {url: search_autocomplete_path } }
- = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' }
+ .dropdown{ data: { url: search_autocomplete_path } }
+ = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
.dropdown-menu.dropdown-select
= dropdown_content do
%ul
@@ -21,8 +24,9 @@
%i.search-icon
%i.clear-icon.js-clear-input
- = hidden_field_tag :group_id, @group.try(:id)
- = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
+ = hidden_field_tag :group_id, @group.try(:id), class: 'js-search-group-options', data: group_data_attrs
+
+ = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id', class: 'js-search-project-options', data: project_data_attrs
- if @project && @project.persisted?
- if current_controller?(:issues)
@@ -36,31 +40,6 @@
- else
= hidden_field_tag :search_code, true
- :javascript
- gl.projectOptions = gl.projectOptions || {};
- gl.projectOptions["#{j(@project.path)}"] = {
- issuesPath: "#{namespace_project_issues_path(@project.namespace, @project)}",
- mrPath: "#{namespace_project_merge_requests_path(@project.namespace, @project)}",
- name: "#{j(@project.name)}"
- };
-
- - if @group && @group.persisted? && @group.path
- :javascript
- gl.groupOptions = gl.groupOptions || {};
- gl.groupOptions["#{j(@group.path)}"] = {
- name: "#{j(@group.name)}",
- issuesPath: "#{issues_group_path(j(@group.path))}",
- mrPath: "#{merge_requests_group_path(j(@group.path))}"
- };
-
-
- :javascript
- gl.dashboardOptions = {
- issuesPath: "#{issues_dashboard_url}",
- mrPath: "#{merge_requests_dashboard_url}"
- };
-
-
- if @snippet || @snippets
= hidden_field_tag :snippets, true
= hidden_field_tag :repository_ref, @ref
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index f7012595a5a..e44a2bfed9d 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -47,7 +47,7 @@
Repository
- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :environments]) do
+ = nav_link(controller: [:pipelines, :builds, :environments, :cycle_analytics]) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
@@ -113,3 +113,7 @@
%li.hidden
= link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
Commits
+
+ -# Shortcut to issue boards
+ %li.hidden
+ = link_to 'Issue Boards', namespace_project_board_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index dde2e2889dc..1ec4c3f0c67 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -25,8 +25,8 @@
- if @labels_url
adjust your #{link_to 'label subscriptions', @labels_url}.
- else
- - if @sent_notification && @sent_notification.unsubscribable?
- = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
+ - if @sent_notification_url
+ = link_to "unsubscribe", @sent_notification_url
from this thread or
adjust your notification settings.
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 9fe94291db7..277eb71ea73 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -14,9 +14,6 @@
window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
window.preview_markdown_path = "#{preview_markdown_path}";
-- content_for :scripts_body do
- = render "layouts/init_auto_complete" if current_user
-
- content_for :header_content do
.js-dropdown-menu-projects
.dropdown-menu.dropdown-select.dropdown-menu-projects
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index a42b3b8eb38..93187873501 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,4 +1,5 @@
- page_title "SSH Keys"
+= render 'profiles/head'
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index d9fa74fad90..578af9fe98d 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -87,6 +87,9 @@
= f.label :location, 'Location', class: "label-light"
= f.text_field :location, class: "form-control"
.form-group
+ = f.label :organization, 'Organization', class: "label-light"
+ = f.text_field :organization, class: "form-control"
+ .form-group
= f.label :bio, class: "label-light"
= f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
%span.help-block Tell us about yourself in fewer than 250 characters.
diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml
index 249680bcab6..de1337a2a24 100644
--- a/app/views/profiles/update_username.js.haml
+++ b/app/views/profiles/update_username.js.haml
@@ -1,6 +1,6 @@
- if @user.valid?
:plain
- new Flash("Username sucessfully changed", "notice")
+ new Flash("Username successfully changed", "notice")
- else
:plain
new Flash("Username change failed - #{@user.errors.full_messages.first}", "alert")
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index ac50ce83f6a..d011e51e696 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,13 +1,16 @@
-.nav-block.activity-filter-block
- - if current_user
- .controls
- = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
- %i.fa.fa-rss
+- @no_container = true
- = render 'shared/event_filter'
+%div{ class: container_class }
+ .nav-block.activity-filter-block
+ - if current_user
+ .controls
+ = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
+ = icon('rss')
-.content_list.project-activity{:"data-href" => activity_project_path(@project)}
-= spinner
+ = render 'shared/event_filter'
+
+ .content_list.project-activity{:"data-href" => activity_project_path(@project)}
+ = spinner
:javascript
var activity = new Activities();
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 3c6b931f41a..1c3bccccb5c 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,6 +1,6 @@
- if event = last_push_event
- if show_last_push_widget?(event)
- .row-content-block.top-block.clear-block.hidden-xs
+ .row-content-block.top-block.hidden-xs.white
%div{ class: container_class }
.event-last-push
.event-last-push-text
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index 19b4249374b..80053dd501b 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -1,11 +1,14 @@
-%fieldset.builds-feature
- %h5.prepend-top-0
- Merge Requests
- .form-group
- .checkbox
- = f.label :only_allow_merge_if_build_succeeds do
- = f.check_box :only_allow_merge_if_build_succeeds
- %strong Only allow merge requests to be merged if the build succeeds
- .help-block
- Builds need to be configured to enable this feature.
- = link_to icon('question-circle'), help_page_path('workflow/merge_requests', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
+.merge-requests-feature
+ %fieldset.builds-feature
+ %hr
+ %h5.prepend-top-0
+ Merge Requests
+ .form-group
+ .checkbox
+ = f.label :only_allow_merge_if_build_succeeds do
+ = f.check_box :only_allow_merge_if_build_succeeds
+ %strong Only allow merge requests to be merged if the build succeeds
+ %br
+ %span.descr
+ Builds need to be configured to enable this feature.
+ = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 3978fa60d66..cb97181b9e1 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -7,3 +7,6 @@
= text_area_tag attr, nil, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
= icon('compress')
+
+- content_for :scripts_body do
+ = render "layouts/init_auto_complete" if current_user && (@target_project || @project)
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 377665b096f..5a98e258b22 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -11,7 +11,7 @@
%small= number_to_human_size @blob.size
.file-actions
= render "projects/blob/actions"
- .file-content.blame.code.js-syntax-highlight
+ .table-responsive.file-content.blame.code.js-syntax-highlight
%table
- current_line = 1
- @blame_groups.each do |blame_group|
@@ -19,6 +19,7 @@
%td.blame-commit
.commit
- commit = blame_group[:commit]
+ = author_avatar(commit, size: 36)
.commit-row-title
%strong
= link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark"
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 0237e152b54..d4f59764a70 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -21,6 +21,13 @@
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
= dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
+ = button_tag class: 'soft-wrap-toggle btn', type: 'button' do
+ %span.no-wrap
+ = custom_icon('icon_no_wrap')
+ No wrap
+ %span.soft-wrap
+ = custom_icon('icon_soft_wrap')
+ Soft wrap
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 808e6b95746..5217b8bf028 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -3,6 +3,7 @@
- diverging_commit_counts = @repository.diverging_commit_counts(branch)
- number_commits_behind = diverging_commit_counts[:behind]
- number_commits_ahead = diverging_commit_counts[:ahead]
+- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
%li(class="js-branch-#{branch.name}")
%div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do
@@ -19,12 +20,12 @@
%i.fa.fa-lock
protected
.controls.hidden-xs
- - if create_mr_button?(@repository.root_ref, branch.name)
+ - if merge_project && create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
Merge Request
- if branch.name != @repository.root_ref
- = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do
+ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: "btn btn-default #{'prepend-left-10' unless merge_project}", method: :post, title: "Compare" do
Compare
= render 'projects/buttons/download', project: @project, ref: branch.name
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 5b0b58e087b..f5344091cae 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -1,3 +1,6 @@
+- builds = @build.pipeline.builds.latest.to_a
+- statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
+
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
Build
@@ -5,46 +8,12 @@
%a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
= icon('angle-double-right')
- if @build.coverage
- .block.block-first
+ .block.coverage
.title
Test coverage
%p.build-detail-row
#{@build.coverage}%
- - builds = @build.pipeline.builds.latest.to_a
- - statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
- - if builds.size > 1
- .dropdown.build-dropdown
- .build-light-text Stage
- %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
- %span.stage-selection More
- = icon('caret-down')
- %ul.dropdown-menu
- - builds.map(&:stage).uniq.each do |stage|
- %li
- %a.stage-item= stage
-
- .builds-container
- - statuses.each do |build_status|
- - builds.select{|build| build.status == build_status}.each do |build|
- .build-job{class: ('active' if build == @build), data: {stage: build.stage}}
- = link_to namespace_project_build_path(@project.namespace, @project, build) do
- = icon('check')
- = 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.
-
.blocks-container
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block{ class: ("block-first" if !@build.coverage) }
@@ -76,7 +45,7 @@
.title
Build details
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post
+ = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- if @build.merge_request
%p.build-detail-row
%span.build-light-text Merge Request:
@@ -100,7 +69,7 @@
- elsif @build.runner
\##{@build.runner.id}
.btn-group.btn-group-justified{ role: :group }
- - if @build.has_trace?
+ - if @build.has_trace_file?
= link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
- if @build.active?
= link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
@@ -121,12 +90,13 @@
- if @build.trigger_request.variables
%p
- %span.build-light-text Variables:
+ %button.btn.group.btn-group-justified.reveal-variables Reveal Variables
- @build.trigger_request.variables.each do |key, value|
- %code
- #{key}=#{value}
+ .hide.js-build
+ .js-build-variable= key
+ .js-build-value= value
.block
.title
@@ -141,3 +111,35 @@
- @build.tag_list.each do |tag|
%span.label.label-primary
= tag
+
+ - if @build.pipeline.stages.many?
+ .dropdown.build-dropdown
+ .title Stage
+ %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.stage-selection More
+ = icon('caret-down')
+ %ul.dropdown-menu
+ - @build.pipeline.stages.each do |stage|
+ %li
+ %a.stage-item= stage
+
+ .builds-container
+ - statuses.each do |build_status|
+ - builds.select{|build| build.status == build_status}.each do |build|
+ .build-job{class: ('active' if build == @build), data: {stage: build.stage}}
+ = link_to namespace_project_build_path(@project.namespace, @project, build) do
+ = icon('right-arrow')
+ = 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.
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml
new file mode 100644
index 00000000000..c2bcfb773a6
--- /dev/null
+++ b/app/views/projects/builds/_table.html.haml
@@ -0,0 +1,24 @@
+- admin = local_assigns.fetch(:admin, false)
+
+- if builds.blank?
+ %li
+ .nothing-here-block No builds to show
+- else
+ .table-holder
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build
+ - if admin
+ %th Project
+ %th Runner
+ %th Stage
+ %th Name
+ %th
+ %th Coverage
+ %th
+
+ = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: admin || project.build_coverage_enabled?, admin: admin }
+
+ = paginate builds, theme: 'gitlab'
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 2af625f69cd..5c60b7a7364 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -4,30 +4,8 @@
%div{ class: container_class }
.top-area
- %ul.nav-links
- %li{class: ('active' if @scope.nil?)}
- = link_to project_builds_path(@project) do
- All
- %span.badge.js-totalbuilds-count
- = number_with_delimiter(@all_builds.count(:id))
-
- %li{class: ('active' if @scope == 'pending')}
- = link_to project_builds_path(@project, scope: :pending) do
- Pending
- %span.badge
- = number_with_delimiter(@all_builds.pending.count(:id))
-
- %li{class: ('active' if @scope == 'running')}
- = link_to project_builds_path(@project, scope: :running) do
- Running
- %span.badge
- = number_with_delimiter(@all_builds.running.count(:id))
-
- %li{class: ('active' if @scope == 'finished')}
- = link_to project_builds_path(@project, scope: :finished) do
- Finished
- %span.badge
- = number_with_delimiter(@all_builds.finished.count(:id))
+ - build_path_proc = ->(scope) { project_builds_path(@project, scope: scope) }
+ = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
.nav-controls
- if can?(current_user, :update_build, @project)
@@ -42,23 +20,4 @@
%span CI Lint
%ul.content-list.builds-content-list
- - if @builds.blank?
- %li
- .nothing-here-block No builds to show
- - else
- .table-holder
- %table.table.builds
- %thead
- %tr
- %th Status
- %th Commit
- %th Stage
- %th Name
- %th
- - if @project.build_coverage_enabled?
- %th Coverage
- %th
-
- = render @builds, commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled?
-
- = paginate @builds, theme: 'gitlab'
+ = render "table", builds: @builds, project: @project
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 5f5e071eb40..24de020917a 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -37,6 +37,6 @@
%li.dropdown-header Previous Artifacts
- artifacts.each do |job|
%li
- = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do
+ = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do
%i.fa.fa-download
%span Download '#{job.name}'
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index d78888e9fe4..22db33498f1 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -3,11 +3,11 @@
- 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
= custom_icon('icon_fork')
- Fork
+ %span Fork
- else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do
= custom_icon('icon_fork')
- Fork
+ %span Fork
%div.count-with-arrow
%span.arrow
= link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 73de8abe55b..75192c48188 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -1,3 +1,11 @@
+- admin = local_assigns.fetch(:admin, false)
+- ref = local_assigns.fetch(:ref, nil)
+- commit_sha = local_assigns.fetch(:commit_sha, nil)
+- retried = local_assigns.fetch(:retried, false)
+- stage = local_assigns.fetch(:stage, false)
+- coverage = local_assigns.fetch(:coverage, false)
+- allow_retry = local_assigns.fetch(:allow_retry, false)
+
%tr.build.commit
%td.status
- if can?(current_user, :read_build, build)
@@ -9,11 +17,11 @@
.branch-commit
- if can?(current_user, :read_build, build)
= link_to namespace_project_build_url(build.project.namespace, build.project, build) do
- %span ##{build.id}
+ %span.build-link ##{build.id}
- else
- %span ##{build.id}
+ %span.build-link ##{build.id}
- - if defined?(ref) && ref
+ - if ref
- if build.ref
.icon-container
= build.tag? ? icon('tag') : icon('code-fork')
@@ -23,12 +31,12 @@
.icon-container
= custom_icon("icon_commit")
- - if defined?(commit_sha) && commit_sha
+ - if commit_sha
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace"
- if build.stuck?
= icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
- - if defined?(retried) && retried
+ - if retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
.label-container
@@ -40,19 +48,24 @@
%span.label.label-info triggered
- if build.try(:allow_failure)
%span.label.label-danger allowed to fail
- - if defined?(retried) && retried
+ - if retried
%span.label.label-warning retried
- if build.manual?
%span.label.label-info manual
- - if defined?(runner) && runner
+ - if admin
+ %td
+ - if build.project
+ = link_to build.project.name_with_namespace, admin_namespace_project_path(build.project.namespace, build.project)
+
+ - if admin
%td
- if build.try(:runner)
= runner_link(build.runner)
- else
.light none
- - if defined?(stage) && stage
+ - if stage
%td
= build.stage
@@ -64,13 +77,14 @@
%p.duration
= custom_icon("icon_timer")
= duration_in_numbers(build.duration)
+
- if build.finished_at
%p.finished-at
= icon("calendar")
%span #{time_ago_with_tooltip(build.finished_at)}
- - if defined?(coverage) && coverage
- %td.coverage
+ %td.coverage
+ - if coverage
- if build.try(:coverage)
#{build.coverage}%
@@ -83,10 +97,10 @@
- if build.active?
= link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
= icon('remove', class: 'cred')
- - elsif defined?(allow_retry) && allow_retry
+ - elsif allow_retry
- if build.retryable?
= link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
= icon('repeat')
- - elsif build.playable?
+ - elsif build.playable? && !admin
= link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
= custom_icon('icon_play')
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 36fb0300aeb..547bc0c9c19 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -1,15 +1,12 @@
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
-%li.build{class: ("playable" if is_playable)}
- .curve
- .build-content
- - if is_playable
- = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
- = render_status_with_link('build', 'play')
- %span.ci-status-text= subject.name
- - elsif can?(current_user, :read_build, @project)
- = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
- = render_status_with_link('build', subject.status)
- %span.ci-status-text= subject.name
- - else
- = render_status_with_link('build', subject.status)
- = ci_icon_for_status(subject.status)
+- if is_playable
+ = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
+ = render_status_with_link('build', 'play')
+ .ci-status-text= subject.name
+- elsif can?(current_user, :read_build, @project)
+ = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
+ = render_status_with_link('build', subject.status)
+ .ci-status-text= subject.name
+- else
+ = render_status_with_link('build', subject.status)
+ = ci_icon_for_status(subject.status)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index bb9493f5158..04e48a4dc17 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -1,4 +1,7 @@
- status = pipeline.status
+- show_commit = local_assigns.fetch(:show_commit, true)
+- show_branch = local_assigns.fetch(:show_branch, true)
+
%tr.commit
%td.commit-link
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
@@ -10,14 +13,14 @@
.branch-commit
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
%span ##{pipeline.id}
- - if pipeline.ref
- - unless defined?(hide_branch) && hide_branch
- .icon-container
- = pipeline.tag? ? icon('tag') : icon('code-fork')
- = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
- .icon-container
- = custom_icon("icon_commit")
- = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
+ - if pipeline.ref && show_branch
+ .icon-container
+ = pipeline.tag? ? icon('tag') : icon('code-fork')
+ = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
+ - if show_commit
+ .icon-container
+ = custom_icon("icon_commit")
+ = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
- if pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
- if pipeline.triggered?
@@ -36,16 +39,14 @@
- stages_status = pipeline.statuses.relevant.latest.stages_status
- - stages.each do |stage|
- %td.stage-cell
+ %td.stage-cell
+ - stages.each do |stage|
- status = stages_status[stage]
- tooltip = "#{stage.titleize}: #{status || 'not found'}"
- if status
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
- = ci_icon_for_status(status)
- - else
- .light.has-tooltip{ title: tooltip }
- \-
+ .stage-container
+ = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
+ = ci_icon_for_status(status)
%td
- if pipeline.duration
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
index a508382578a..b7087749428 100644
--- a/app/views/projects/commit/_builds.html.haml
+++ b/app/views/projects/commit/_builds.html.haml
@@ -1,2 +1,2 @@
-- @pipelines.each do |pipeline|
+- @ci_pipelines.each do |pipeline|
= render "pipeline", pipeline: pipeline, pipeline_details: true
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index 935433306ea..cbfd99ca448 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -3,6 +3,11 @@
= link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Changes
%span.badge= @diffs.size
+ - if can?(current_user, :read_pipeline, @project)
+ = nav_link(path: 'commit#pipelines') do
+ = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
+ Pipelines
+ %span.badge= @ci_pipelines.count
= nav_link(path: 'commit#builds') do
= link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Builds
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 20a85148ab5..9258f4b3c25 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -39,8 +39,7 @@
= stage.titleize
.builds-container
%ul
- - statuses.each do |status|
- = render "projects/#{status.to_partial_path}_pipeline", subject: status
+ = render "projects/commit/pipeline_stage", statuses: statuses
- if pipeline.yaml_errors.present?
diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml
new file mode 100644
index 00000000000..23c5c51fbc2
--- /dev/null
+++ b/app/views/projects/commit/_pipeline_stage.html.haml
@@ -0,0 +1,14 @@
+- status_groups = statuses.group_by(&:group_name)
+- status_groups.each do |group_name, grouped_statuses|
+ - if grouped_statuses.one?
+ - status = grouped_statuses.first
+ - is_playable = status.playable? && can?(current_user, :update_build, @project)
+ %li.build{ class: ("playable" if is_playable) }
+ .curve
+ .build-content
+ = render "projects/#{status.to_partial_path}_pipeline", subject: status
+ - else
+ %li.build
+ .curve
+ .build-content
+ = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml
new file mode 100644
index 00000000000..4e7a6f1af08
--- /dev/null
+++ b/app/views/projects/commit/_pipeline_status_group.html.haml
@@ -0,0 +1,11 @@
+- group_status = CommitStatus.where(id: subject).status
+= render_status_with_link('build', group_status)
+.dropdown.inline
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %span.ci-status-text
+ = name
+ %span.badge= subject.size
+ %ul.dropdown-menu.grouped-pipeline-dropdown
+ .arrow
+ - subject.each do |status|
+ = render "projects/#{status.to_partial_path}_pipeline", subject: status
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index f41a11a056d..998812793a2 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -7,14 +7,8 @@
%table.table.builds
%tbody
%th Status
- %th Commit
- - pipelines.stages.each do |stage|
- %th.stage
- - if stage.titleize.length > 12
- %span.has-tooltip{ title: "#{stage.titleize}" }
- = stage.titleize
- - else
- = stage.titleize
+ %th Pipeline
+ %th Stages
%th
%th
- = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, hide_branch: true
+ = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, show_commit: false
diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml
new file mode 100644
index 00000000000..d85d6729a81
--- /dev/null
+++ b/app/views/projects/commit/pipelines.html.haml
@@ -0,0 +1,7 @@
+- page_title "Pipelines", "#{@commit.title} (#{@commit.short_id})", "Commits"
+
+.prepend-top-default
+ = render "commit_box"
+
+= render "ci_menu"
+= render "pipelines_list", pipelines: @ci_pipelines
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index 4d1ee1c5318..80763ce67ca 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,27 +1,28 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
- = link_to project_files_path(@project) do
- Files
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
+ = link_to project_files_path(@project) do
+ Files
- = nav_link(controller: [:commit, :commits]) do
- = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
- Commits
+ = nav_link(controller: [:commit, :commits]) do
+ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
+ Commits
- = nav_link(controller: %w(network)) do
- = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do
- Network
+ = nav_link(controller: %w(network)) do
+ = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do
+ Network
- = nav_link(controller: :compare) do
- = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
- Compare
+ = nav_link(controller: :compare) do
+ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
+ Compare
- = nav_link(html_options: {class: branches_tab_class}) do
- = link_to namespace_project_branches_path(@project.namespace, @project) do
- Branches
+ = nav_link(html_options: {class: branches_tab_class}) do
+ = link_to namespace_project_branches_path(@project.namespace, @project) do
+ Branches
- = nav_link(controller: [:tags, :releases]) do
- = link_to namespace_project_tags_path(@project.namespace, @project) do
- Tags
+ = nav_link(controller: [:tags, :releases]) do
+ = link_to namespace_project_tags_path(@project.namespace, @project) do
+ Tags
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 9a44ba94970..876c8002627 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -5,7 +5,8 @@
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
-= render "head"
+= content_for :sub_nav do
+ = render "head"
%div{ class: container_class }
.row-content-block.second-block.content-component-block
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index d79336f5a60..76b68c544aa 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,17 +1,22 @@
= form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do
.clearfix
- if params[:to] && params[:from]
- = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
- .form-group.dropdown.compare-form-group.js-compare-from-dropdown
+ .compare-switch-container
+ = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
+ .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
.input-group.inline-input-group
%span.input-group-addon from
- = text_field_tag :from, params[:from], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from].presence }
+ = hidden_field_tag :from, params[:from]
+ = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
+ .dropdown-toggle-text= params[:from] || 'Select branch/tag'
= render "ref_dropdown"
- = "..."
- .form-group.dropdown.compare-form-group.js-compare-to-dropdown
+ .compare-ellipsis ...
+ .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
%span.input-group-addon to
- = text_field_tag :to, params[:to], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to].presence }
+ = hidden_field_tag :to, params[:to]
+ = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
+ .dropdown-toggle-text= params[:to] || 'Select branch/tag'
= render "ref_dropdown"
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
diff --git a/app/views/projects/compare/_ref_dropdown.html.haml b/app/views/projects/compare/_ref_dropdown.html.haml
index c604c6d0135..27d928c87a0 100644
--- a/app/views/projects/compare/_ref_dropdown.html.haml
+++ b/app/views/projects/compare/_ref_dropdown.html.haml
@@ -1,4 +1,5 @@
.dropdown-menu.dropdown-menu-selectable
= dropdown_title "Select branch/tag"
+ = dropdown_filter "Filter by branch/tag"
= dropdown_content
= dropdown_loading
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
new file mode 100644
index 00000000000..7f346df8797
--- /dev/null
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -0,0 +1,59 @@
+- @no_container = true
+- page_title "Cycle Analytics"
+= render "projects/pipelines/head"
+
+#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}}
+
+ .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"}
+ = icon('times', class: 'dismiss-icon', "@click": "dismissLanding()")
+ .row
+ .col-sm-3.col-xs-12.svg-container
+ = custom_icon('icon_cycle_analytics_splash')
+ .col-sm-8.col-xs-12.inner-content
+ %h4
+ Introducing Cycle Analytics
+ %p
+ Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
+
+ = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
+
+ = icon("spinner spin", "v-show" => "isLoading")
+
+ .wrapper{"v-show" => "!isLoading && !hasError"}
+ .panel.panel-default
+ .panel-heading
+ Pipeline Health
+
+ .content-block
+ .container-fluid
+ .row
+ .col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"}
+ %h3.header {{item.value}}
+ %p.text {{item.title}}
+
+ .col-sm-3.col-xs-12.column
+ .dropdown.inline.js-ca-dropdown
+ %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
+ %span.dropdown-label Last 30 days
+ %i.fa.fa-chevron-down
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ %a{'href' => "#", 'data-value' => '30'}
+ Last 30 days
+ %li
+ %a{'href' => "#", 'data-value' => '90'}
+ Last 90 days
+
+ .bordered-box
+ %ul.content-list
+ %li{"v-for" => "item in analytics.stats"}
+ .container-fluid
+ .row
+ .col-xs-8.title-col
+ %p.title
+ {{item.title}}
+ %p.text
+ {{item.description}}
+ .col-xs-4.value-col
+ %span
+ {{item.value}}
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index d37961c4e40..779c8ea0104 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -11,7 +11,9 @@
- elsif diff_file.collapsed?
- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path))
.nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
- This diff is collapsed. Click to expand it.
+ This diff is collapsed.
+ %a.click-to-expand
+ Click to expand it.
- elsif diff_file.diff_lines.length > 0
- if diff_view == :parallel
= render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index ad2eb3e504f..d07de45fdde 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -5,7 +5,7 @@
- unless diff_file.submodule?
.file-actions.hidden-xs
- if blob_text_viewable?(blob)
- = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file" do
+ = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
= icon('comment')
\
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index 95a2772fd0b..a6a2e5690b5 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -1,3 +1,4 @@
+%i.fa.diff-toggle-caret
- if defined?(blob) && blob && diff_file.submodule?
%span
= icon('archive fw')
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index f6d751a343e..a04d53e02bf 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -84,15 +84,14 @@
= project_feature_access_select(:snippets_access_level)
- if Gitlab.config.lfs.enabled && current_user.admin?
- .form-group
- .checkbox
- = f.label :lfs_enabled do
- = f.check_box :lfs_enabled, checked: @project.lfs_enabled?
- %strong LFS
- %br
- %span.descr
- Git Large File Storage
- = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ .row
+ .col-md-9
+ = f.label :lfs_enabled, 'LFS', class: 'label-light'
+ %span.help-block
+ Git Large File Storage
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ .col-md-3
+ = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control'
- if Gitlab.config.registry.enabled
.form-group
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 636beb73ec2..7a39064adc5 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -23,6 +23,8 @@
or a
= link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore'), class: 'underlined-link'
to this project.
+ %p
+ You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected.
- if can?(current_user, :push_code, @project)
%div{ class: container_class }
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index a1d79bdabda..bacc5708e4b 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -32,11 +32,11 @@
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do
= custom_icon('icon_fork')
- Fork
+ %span Fork
- else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-new' do
= custom_icon('icon_fork')
- Fork
+ %span Fork
= render 'projects', projects: @forks
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
index 576d0bec51b..409f4701e4b 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -1,10 +1,7 @@
-%li.build
- .curve
- .build-content
- - if subject.target_url
- - link_to subject.target_url do
- = render_status_with_link('commit status', subject.status)
- %span.ci-status-text= subject.name
- - else
- = render_status_with_link('commit status', subject.status)
- %span.ci-status-text= subject.name
+- if subject.target_url
+ = link_to subject.target_url do
+ = render_status_with_link('commit status', subject.status)
+ %span.ci-status-text= subject.name
+- else
+ = render_status_with_link('commit status', subject.status)
+ %span.ci-status-text= subject.name
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index 082e2cb4d8c..1a62a6a809c 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,18 +1,19 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
- - content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/chart.js')
- = page_specific_javascript_tag('graphs/graphs_bundle.js')
- = 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
- = nav_link(action: :languages) do
- = link_to 'Languages', languages_namespace_project_graph_path
- - if @project.feature_available?(:builds, current_user)
- = nav_link(action: :ci) do
- = link_to ci_namespace_project_graph_path do
- Continuous Integration
+ - content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/chart.js')
+ = page_specific_javascript_tag('graphs/graphs_bundle.js')
+ = 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
+ = nav_link(action: :languages) do
+ = link_to 'Languages', languages_namespace_project_graph_path
+ - if @project.feature_available?(:builds, current_user)
+ = nav_link(action: :ci) do
+ = link_to ci_namespace_project_graph_path do
+ Continuous Integration
diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml
index 3fcf1692e09..ceabe2eab3d 100644
--- a/app/views/projects/hooks/_project_hook.html.haml
+++ b/app/views/projects/hooks/_project_hook.html.haml
@@ -3,7 +3,7 @@
.col-md-8.col-lg-7
%strong.light-header= hook.url
%div
- - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger|
+ - %w(push_events tag_push_events issues_events confidential_issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray.deploy-project-label= trigger.titleize
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
index f88b33018d0..509b01c548a 100644
--- a/app/views/projects/issues/_head.html.haml
+++ b/app/views/projects/issues/_head.html.haml
@@ -1,32 +1,33 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
- = nav_link(controller: :issues) do
- = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
- %span
- Issues
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
+ = nav_link(controller: :issues) do
+ = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
+ %span
+ Issues
- = nav_link(controller: :boards) do
- = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
- %span
- Board
+ = nav_link(controller: :boards) do
+ = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
+ %span
+ Board
- - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
- = nav_link(controller: :merge_requests) do
- = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
- %span
- Merge Requests
+ - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
+ = nav_link(controller: :merge_requests) do
+ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
+ %span
+ Merge Requests
- - if project_nav_tab? :labels
- = nav_link(controller: :labels) do
- = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
- %span
- Labels
+ - if project_nav_tab? :labels
+ = nav_link(controller: :labels) do
+ = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
+ %span
+ Labels
- - if project_nav_tab? :milestones
- = nav_link(controller: :milestones) do
- = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
- %span
- Milestones \ No newline at end of file
+ - if project_nav_tab? :milestones
+ = nav_link(controller: :milestones) do
+ = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
+ %span
+ Milestones
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 79b14819865..8b1a8a8a2d9 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -1,7 +1,7 @@
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
- - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
+ - if @bulk_edit
.issue-check
- = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
+ = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
.issue-title.title
%span.issue-title-text
@@ -29,7 +29,7 @@
- note_count = issue.notes.user.count
%li
- = link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do
+ = link_to issue_path(issue, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
= icon('comments')
= note_count
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index f34f3c05737..a2c31c0b4c5 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,4 +1,4 @@
-%ul.content-list.issues-list
+%ul.content-list.issues-list.issuable-list
= render @issues
- if @issues.blank?
%li
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index d8075371853..31d3ec23276 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -1,7 +1,7 @@
- if @merge_requests.any?
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
- %ul.unstyled-list
+ %ul.unstyled-list.related-merge-requests
- has_any_ci = @merge_requests.any?(&:pipeline)
- @merge_requests.each do |merge_request|
%li
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 24749699c6d..c56b6cc11f5 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,13 +1,12 @@
- if can?(current_user, :push_code, @project)
.pull-right
- #new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
+ #new-branch.new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
+ = link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do
+ = icon('spinner spin')
+ Checking branches
= link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid),
- method: :post, class: 'btn btn-new btn-inverted has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do
- .checking
- = icon('spinner spin')
- Checking branches
- .available.hide
- New branch
- .unavailable.hide
- = icon('exclamation-triangle')
- New branch unavailable
+ method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do
+ New branch
+ = link_to '#', class: 'unavailable btn btn-grouped hide', disabled: 'disabled' do
+ = icon('exclamation-triangle')
+ New branch unavailable
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index a8eeab3e55e..44683c8bcdb 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -1,7 +1,7 @@
- if @related_branches.any?
%h2.related-branches-title
= pluralize(@related_branches.count, 'Related Branch')
- %ul.unstyled-list
+ %ul.unstyled-list.related-merge-requests
- @related_branches.each do |branch|
%li
- target = @project.repository.find_branch(branch).target
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 1a87045aa60..cc57cfdb342 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -1,7 +1,10 @@
- @no_container = true
+- @bulk_edit = can?(current_user, :admin_issue, @project)
+
- page_title "Issues"
- new_issue_email = @project.new_issue_address(current_user)
-= render "projects/issues/head"
+= content_for :sub_nav do
+ = render "projects/issues/head"
= content_for :meta_tags do
- if current_user
diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml
index aa143e54ffe..6ab6ae50389 100644
--- a/app/views/projects/labels/_form.html.haml
+++ b/app/views/projects/labels/_form.html.haml
@@ -14,7 +14,7 @@
.col-sm-10
.input-group
.input-group-addon.label-color-preview &nbsp;
- = f.color_field :color, class: "form-control"
+ = f.text_field :color, class: "form-control"
.help-block
Choose any color.
%br
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index d070979bcfe..cfb44bd206c 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -2,10 +2,10 @@
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
= link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- - if @merge_request.closed?
+ - if @merge_request.reopenable?
= link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
%comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" }
- %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { namespace_path: "#{@merge_request.project.namespace.path}", project_path: "#{@merge_request.project.path}" } }
+ %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
{{ buttonText }}
#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 5029b365f93..68fb7d5a414 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,4 +1,8 @@
%li{ class: mr_css_classes(merge_request) }
+ - if @bulk_edit
+ .issue-check
+ = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
+
.merge-request-title.title
%span.merge-request-title-text
= link_to merge_request.title, merge_request_path(merge_request)
@@ -37,7 +41,7 @@
- note_count = merge_request.mr_and_commit_notes.user.count
%li
- = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do
+ = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
= icon('comments')
= note_count
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index 446887774a4..fe82f751f53 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -1,4 +1,4 @@
-%ul.content-list.mr-list
+%ul.content-list.mr-list.issuable-list
= render @merge_requests
- if @merge_requests.blank?
%li
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 9d8b4cc56be..d03ff9ec7e8 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -29,17 +29,19 @@
%ul.dropdown-menu.dropdown-menu-align-right
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
- .normal
- %span Request to merge
- %span.label-branch= source_branch_with_namespace(@merge_request)
- %span into
- %span.label-branch
- = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
- - if @merge_request.open? && @merge_request.diverged_from_target_branch?
- %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
+ - unless @merge_request.closed_without_fork?
+ .normal
+ %span Request to merge
+ %span.label-branch= source_branch_with_namespace(@merge_request)
+ %span into
+ %span.label-branch
+ = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
+ - if @merge_request.open? && @merge_request.diverged_from_target_branch?
+ %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
- = render "projects/merge_requests/show/how_to_merge"
- = render "projects/merge_requests/widget/show.html.haml"
+ - unless @merge_request.closed_without_source_project?
+ = render "projects/merge_requests/show/how_to_merge"
+ = render "projects/merge_requests/widget/show.html.haml"
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
.light.prepend-top-default.append-bottom-default
@@ -53,10 +55,11 @@
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
Discussion
%span.badge= @merge_request.mr_and_commit_notes.user.count
- %li.commits-tab
- = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
- Commits
- %span.badge= @commits_count
+ - unless @merge_request.closed_without_source_project?
+ %li.commits-tab
+ = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
+ Commits
+ %span.badge= @commits_count
- if @pipeline
%li.pipelines-tab
= link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
@@ -83,7 +86,7 @@
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
- .content-block.content-block-small.oneline-block
+ .content-block.content-block-small
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.row
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index ace275c689b..144b3a9c8c8 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- @bulk_edit = can?(current_user, :admin_merge_request, @project)
+
- page_title "Merge Requests"
= render "projects/issues/head"
= render 'projects/last_push'
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 b727efaa6a6..f1d5441f9dd 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
@@ -12,7 +12,7 @@
%pre.dark#merge-info-1
- if @merge_request.for_fork?
:preserve
- git fetch #{h @merge_request.source_project.http_url_to_repo} #{h @merge_request.source_branch}
+ git fetch #{h default_url_to_repo(@merge_request.source_project)} #{h @merge_request.source_branch}
git checkout -b #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} FETCH_HEAD
- else
:preserve
@@ -47,8 +47,9 @@
Note that pushing to GitLab requires write access to this repository.
%p
%strong Tip:
- You can also checkout merge requests locally by
- %a{href: 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/workflow/merge_requests.md#checkout-merge-requests-locally', target: '_blank'} following these guidelines
+ = succeed '.' do
+ You can also checkout merge requests locally by
+ = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank'
:javascript
$(function(){
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 2da70ce7137..904452fcc4f 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -1,31 +1,74 @@
-- merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff
+- if @merge_request_diffs.size > 1
+ .mr-version-controls
+ %div.mr-version-menus-container.content-block
+ Changes between
+ %span.dropdown.inline.mr-version-dropdown
+ %a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} }
+ %span
+ - if @merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(@merge_request_diff)}
+ %span.caret
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Version:
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times', class: 'dropdown-menu-close-icon')
+ .dropdown-content
+ %ul
+ - @merge_request_diffs.each do |merge_request_diff|
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
+ %strong
+ - if merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(merge_request_diff)}
+ .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+ %small
+ #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
+ = time_ago_with_tooltip(merge_request_diff.created_at)
-- if merge_request_diffs.size > 1
- .mr-version-switch
- Version:
- %span.dropdown.inline
- %a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
- %strong.monospace<
- - if @merge_request_diff.latest?
- #{"latest"}
- - else
- #{@merge_request_diff.head_commit.short_id}
- %span.caret
- %ul.dropdown-menu.dropdown-menu-selectable
- - merge_request_diffs.each do |merge_request_diff|
- %li
- = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, diff_id: merge_request_diff.id), class: ('is-active' if merge_request_diff == @merge_request_diff) do
- %strong.monospace
- #{merge_request_diff.head_commit.short_id}
- %br
- %small
- #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
- = time_ago_with_tooltip(merge_request_diff.created_at)
+ - if @merge_request_diff.base_commit_sha
+ and
+ %span.dropdown.inline.mr-version-compare-dropdown
+ %a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} }
+ %span
+ - if @start_sha
+ version #{version_index(@start_version)}
+ - else
+ #{@merge_request.target_branch}
+ %span.caret
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Compared with:
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times', class: 'dropdown-menu-close-icon')
+ .dropdown-content
+ %ul
+ - @comparable_diffs.each do |merge_request_diff|
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
+ %strong
+ - if merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(merge_request_diff)}
+ .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+ %small
+ = time_ago_with_tooltip(merge_request_diff.created_at)
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
+ %strong
+ #{@merge_request.target_branch} (base)
+ .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
- - unless @merge_request_diff.latest?
- %span.prepend-left-default
+ - unless @merge_request_diff.latest? && !@start_sha
+ .comments-disabled-notif.content-block
= icon('info-circle')
- This version is not the latest one. Comments are disabled
- .pull-right
- %span.monospace
- #{@merge_request_diff.base_commit.short_id}..#{@merge_request_diff.head_commit.short_id}
+ - if @start_sha
+ Comments are disabled because you're comparing two versions of this merge request.
+ - else
+ Comments are disabled because you're viewing an old version of this merge request.
+ = link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm'
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 494695a03a5..44e645a7e81 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -43,15 +43,16 @@
= icon("times-circle")
Could not connect to the CI server. Please check your settings and try again.
-- @merge_request.environments.each do |environment|
- .mr-widget-heading
- .ci_widget.ci-success
- = ci_icon_for_status("success")
- %span.hidden-sm
- Deployed to
- = succeed '.' do
- = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment), class: 'environment'
- - external_url = environment.external_url
- - if external_url
- = link_to external_url, target: '_blank' do
- = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
+- @merge_request.environments.sort_by(&:name).each do |environment|
+ - if can?(current_user, :read_environment, environment)
+ .mr-widget-heading
+ .ci_widget.ci-success
+ = ci_icon_for_status("success")
+ %span.hidden-sm
+ Deployed to
+ = succeed '.' do
+ = link_to environment.name, environment_path(environment), class: 'environment'
+ - external_url = environment.external_url
+ - if external_url
+ = link_to external_url, target: '_blank' do
+ = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 0a1e2bb2cc6..fda0592dd41 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -55,7 +55,7 @@
= render 'bitbucket_import_modal'
%div
- if gitlab_import_enabled?
- = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless bitbucket_import_configured?}" do
+ = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
= icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 402f5b52f5e..46b402545cd 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,3 +1,5 @@
+- supports_slash_commands = note_supports_slash_commands?(@note)
+
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
@@ -14,8 +16,8 @@
attr: :note,
classes: 'note-textarea js-note-text',
placeholder: "Write a comment or drag your files here...",
- supports_slash_commands: true
- = render 'projects/notes/hints', supports_slash_commands: true
+ supports_slash_commands: supports_slash_commands
+ = render 'projects/notes/hints', supports_slash_commands: supports_slash_commands
.error-alert
.note-form-actions.clearfix
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 7c82177f9ea..788be4a0047 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -1,6 +1,5 @@
- return unless note.author
- return if note.cross_reference_not_visible_for?(current_user)
-- can_resolve = can?(current_user, :resolve_note, note)
- note_editable = note_editable?(note)
%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
@@ -24,13 +23,13 @@
%span.note-role.hidden-xs= access
- if note.resolvable?
- %resolve-btn{ ":namespace-path" => "'#{note.project.namespace.path}'",
- ":project-path" => "'#{note.project.path}'",
- ":discussion-id" => "'#{note.discussion_id}'",
+ - can_resolve = can?(current_user, :resolve_note, note)
+ %resolve-btn{ "project-path" => "#{project_path(note.project)}",
+ "discussion-id" => "#{note.discussion_id}",
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
- ":resolved-by" => "'#{note.resolved_by.try(:name)}'",
+ "resolved-by" => "#{note.resolved_by.try(:name)}",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"v-ref:note_#{note.id}" => true }
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 74538a9723e..8352eba7446 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -14,9 +14,9 @@
.disabled-comment.text-center
.disabled-comment-text.inline
Please
- = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
+ = link_to "sign up", new_session_path(:user, redirect_to_referer: 'yes')
or
- = link_to "login", new_session_path(:user, redirect_to_referer: 'yes')
+ = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
to post a comment
:javascript
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index f611ddc8f5f..7d421c0e740 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -1,21 +1,28 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- - if project_nav_tab? :pipelines
- = nav_link(controller: :pipelines) do
- = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
- %span
- Pipelines
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ - if project_nav_tab? :pipelines
+ = nav_link(controller: :pipelines) do
+ = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
+ %span
+ Pipelines
- - if project_nav_tab? :builds
- = nav_link(controller: %w(builds)) do
- = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
- %span
- Builds
+ - if project_nav_tab? :builds
+ = nav_link(controller: %w(builds)) do
+ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
+ %span
+ Builds
- - if project_nav_tab? :environments
- = nav_link(controller: %w(environments)) do
- = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
- %span
- Environments
+ - if project_nav_tab? :environments
+ = nav_link(controller: %w(environments)) do
+ = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
+ %span
+ Environments
+
+ - if can?(current_user, :read_cycle_analytics, @project)
+ = nav_link(controller: %w(cycle_analytics)) do
+ = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do
+ %span
+ Cycle Analytics
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 063e83a407a..5800ef7de48 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -10,6 +10,8 @@
- if @pipeline.duration
in
= time_interval_in_words(@pipeline.duration)
+ - if @pipeline.queued_duration
+ = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
.pull-right
= link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 4d957e0d890..50c7e5044b2 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -46,14 +46,8 @@
%table.table.builds
%tbody
%th Status
- %th Commit
- - stages.each do |stage|
- %th.stage
- - if stage.titleize.length > 12
- %span.has-tooltip{ title: "#{stage.titleize}" }
- = stage.titleize
- - else
- = stage.titleize
+ %th Pipeline
+ %th Stages
%th
%th
= render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 833954bc039..37e55dc72a3 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,3 +1,3 @@
:plain
$("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
- new MemberExpirationDate();
+ new gl.MemberExpirationDate();
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
index 0628134b1bb..0193800dedf 100644
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -1,4 +1,4 @@
-%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch), branch_id: protected_branch.id } }
+%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } }
%td
= protected_branch.name
- if @project.root_ref?(protected_branch.name)
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index 8ee2aef0e61..1141168f037 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -5,8 +5,8 @@
:plain
var row = $("table.table_#{@hex_path} tr.file_#{hexdigest(file_name)}");
- row.find("td.tree_time_ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
- row.find("td.tree_commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
+ row.find("td.tree-time-ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
+ row.find("td.tree-commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
- if @more_log_url
:plain
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index c45a9d4f81f..33a9a96183c 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -5,7 +5,7 @@
.col-sm-10
.checkbox
= f.check_box :active
- %span.light Paused runners don't accept new builds
+ %span.light Paused Runners don't accept new builds
.form-group
= label :run_untagged, 'Run untagged jobs', class: 'control-label'
.col-sm-10
@@ -33,6 +33,6 @@
Tags
.col-sm-10
= 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
+ .help-block You can setup jobs to only use Runners with specific tags
.form-actions
= f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 85225857758..6e58e5a0c78 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -15,7 +15,7 @@
.pull-right
- if @project_runners.include?(runner)
- if runner.belongs_to_one_project?
- = link_to 'Remove runner', runner_path(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', namespace_project_runner_project_path(@project.namespace, @project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 9fa4127c948..752b9e060d5 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -1,24 +1,26 @@
-%h3 Shared runners
+%h3 Shared Runners
.bs-callout.bs-callout-warning.shared-runners-description
- if shared_runners_text.present?
= markdown(shared_runners_text, pipeline: 'plain_markdown')
- else
- Shared runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com).
+ GitLab Shared Runners execute code of different projects on the same Runner
+ unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is
+ on GitLab.com).
%hr
- if @project.shared_runners_enabled?
= link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-warning', method: :post do
- Disable shared runners
+ Disable shared Runners
- else
= link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-success', method: :post do
- Enable shared runners
+ Enable shared Runners
&nbsp; for this project
- if @shared_runners_count.zero?
- This GitLab server does not provide any shared runners yet.
- Please use specific runners or ask the administrator to create one.
+ This GitLab server does not provide any shared Runners yet.
+ Please use the specific Runners or ask your administrator to create one.
- else
- %h4.underlined-title Available shared runners - #{@shared_runners_count}
+ %h4.underlined-title Available shared Runners : #{@shared_runners_count}
%ul.bordered-list.available-shared-runners
= render partial: 'runner', collection: @shared_runners, as: :runner
- if @shared_runners_count > 10
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index d469dda5b81..858af78f7bf 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -1,20 +1,20 @@
-%h3 Specific runners
+%h3 Specific Runners
.bs-callout.help-callout
- %h4 How to setup a new project specific runner
+ %h4 How to setup a specific Runner for a new project
%ol
%li
- Install GitLab Runner software.
- Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it
+ Install a Runner compatible with GitLab CI
+ (checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} for information on how to install it).
%li
- Specify the following URL during runner setup:
+ Specify the following URL during the Runner setup:
%code #{ci_root_url(only_path: false)}
%li
Use the following registration token during setup:
%code #{@project.runners_token}
%li
- Start runner!
+ Start the Runner!
- if @project_runners.any?
diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml
index 2d5b9f43c24..92957470070 100644
--- a/app/views/projects/runners/index.html.haml
+++ b/app/views/projects/runners/index.html.haml
@@ -2,24 +2,24 @@
.light.prepend-top-default
%p
- A 'runner' is a process which runs a build.
- You can setup as many runners as you need.
+ A 'Runner' is a process which runs a build.
+ You can setup as many Runners as you need.
%br
Runners can be placed on separate users, servers, and even on your local machine.
- %p Each runner can be in one of the following states:
+ %p Each Runner can be in one of the following states:
%div
%ul
%li
%span.label.label-success active
- \- runner is active and can process any new build
+ \- Runner is active and can process any new builds
%li
%span.label.label-danger paused
- \- runner is paused and will not receive any new build
+ \- Runner is paused and will not receive any new builds
%hr
-%p.lead To start serving your builds you can either add specific runners to your project or use shared runners
+%p.lead To start serving your builds you can either add specific Runners to your project or use shared Runners
.row
.col-sm-6
= render 'specific_runners'
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index bdbf3e5f4d6..9773b8438ec 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,13 +1,13 @@
.hidden-xs
- if can?(current_user, :create_project_snippet, @project)
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do
- New Snippet
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do
+ New snippet
+ - if can?(current_user, :update_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-danger", title: 'Delete Snippet' do
+ 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
Edit
- - if can?(current_user, :update_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-warning", title: 'Delete Snippet' do
- Delete
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
@@ -17,13 +17,13 @@
%ul
- if can?(current_user, :create_project_snippet, @project)
%li
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do
- New Snippet
- - if can?(current_user, :update_project_snippet, @snippet)
- %li
- = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
- Edit
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New snippet" do
+ New snippet
- if can?(current_user, :update_project_snippet, @snippet)
%li
= link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
Delete
+ - if can?(current_user, :update_project_snippet, @snippet)
+ %li
+ = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
+ Edit
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 1646bcf4b8a..e77e1b026f6 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,10 +1,9 @@
- page_title "Snippets"
.sub-header-block
- .pull-right
- - if can?(current_user, :create_project_snippet, @project)
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do
- New Snippet
+ - if can?(current_user, :create_project_snippet, @project)
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
+ New snippet
.oneline
Share code pastes with others out of git repository
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index bae4d8f349f..9503dbded13 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,15 +1,17 @@
- page_title @snippet.title, "Snippets"
-.snippet-holder
- = render 'shared/snippets/header'
+= render 'shared/snippets/header'
- %article.file-holder.file-holder-no-border.snippet-file-content
- .file-title.file-title-clear
+.project-snippets
+ %article.file-holder.snippet-file-content
+ .file-title
= blob_icon 0, @snippet.file_name
= @snippet.file_name
- .file-actions.hidden-xs
+ .file-actions
= clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
= link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
= render 'shared/snippets/blob'
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+
%div#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index a3a4dba3fa4..ee417b58cbf 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -4,6 +4,6 @@
- file_name = blob_item.name
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)), title: file_name do
%span.str-truncated= file_name
- %td.tree_time_ago.cgray
- = render 'projects/tree/spinner'
- %td.hidden-xs.tree_commit
+ %td.hidden-xs.tree-commit
+ %td.tree-time-ago.cgray.text-right
+ = render 'projects/tree/spinner' \ No newline at end of file
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index baaa2caa6de..a1f4e3e8ed6 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,7 +1,7 @@
%article.file-holder.readme-holder
.file-title
= blob_icon readme.mode, readme.name
- = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, @path, readme.name)) do
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, @path, readme.name)) do
%strong
= readme.name
.file-content.wiki
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index ca5d2d7722a..0f7d629ab98 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -4,20 +4,20 @@
%thead
%tr
%th Name
- %th Last Update
- %th.hidden-xs.last-commit
- Last Commit
- .last-commit-content.hidden-sm
+ %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;
= time_ago_with_tooltip(@commit.committed_date)
- &ndash;
= @commit.full_title
- = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right'
-
+ %small.commit-history-link-spacer &#124;
+ = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'commit-history-link'
+ %th.text-right Last Update
- if @path.present?
%tr.tree-item
%td.tree-item-file-name
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index 9577696fc0d..1ccef6d52ab 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -4,6 +4,6 @@
- path = flatten_tree(tree_item)
= link_to namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)), title: path do
%span.str-truncated= path
- %td.tree_time_ago.cgray
- = render 'projects/tree/spinner'
- %td.hidden-xs.tree_commit
+ %td.hidden-xs.tree-commit
+ %td.tree-time-ago.text-right
+ = render 'projects/tree/spinner' \ No newline at end of file
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 37d341212af..9864be3562a 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -4,8 +4,8 @@
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
-= render 'projects/last_push'
= render "projects/commits/head"
+= render 'projects/last_push'
%div{ class: container_class }
.tree-controls
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
index 7f3de47d7df..f6e0b0a7c8a 100644
--- a/app/views/projects/triggers/index.html.haml
+++ b/app/views/projects/triggers/index.html.haml
@@ -4,65 +4,89 @@
.col-lg-3
%h4.prepend-top-0
= page_title
- %p
- Triggers can force a specific branch or tag to rebuild with an API call.
+ %p.prepend-top-20
+ Triggers can force a specific branch or tag to get rebuilt with an API call.
+ %p.append-bottom-0
+ = succeed '.' do
+ Learn more in the
+ = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
.col-lg-9
- %h5.prepend-top-0
- Your triggers
- - if @triggers.any?
- .table-responsive
- %table.table
- %thead
- %th Token
- %th Last used
- %th
- = render partial: 'trigger', collection: @triggers, as: :trigger
- - else
- %p.settings-message.text-center.append-bottom-default
- No triggers have been created yet. Add one using the button below.
+ .panel.panel-default
+ .panel-heading
+ %h4.panel-title
+ Manage your project's triggers
+ .panel-body
+ - if @triggers.any?
+ .table-responsive
+ %table.table
+ %thead
+ %th
+ %strong Token
+ %th
+ %strong Last used
+ %th
+ = render partial: 'trigger', collection: @triggers, as: :trigger
+ - else
+ %p.settings-message.text-center.append-bottom-default
+ No triggers have been created yet. Add one using the button below.
- = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f|
- = f.submit "Add Trigger", class: 'btn btn-success'
+ = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f|
+ = f.submit "Add trigger", class: 'btn btn-success'
- %h5.prepend-top-default
- Use CURL
+ .panel-footer
- %p.light
- Copy the token above, set your branch or tag name, and that reference will be rebuilt.
+ %p
+ In the following examples, you can see the exact API call you need to
+ make in order to rebuild a specific
+ %code ref
+ (branch or tag) with a trigger token.
+ %p
+ All you need to do is replace the
+ %code TOKEN
+ and
+ %code REF_NAME
+ with the trigger token and the branch or tag name respectively.
- %pre
- :plain
- curl -X POST \
- -F token=TOKEN \
- -F ref=REF_NAME \
- #{builds_trigger_url(@project.id)}
- %h5.prepend-top-default
- Use .gitlab-ci.yml
+ %h5.prepend-top-default
+ Use cURL
- %p.light
- In the
- %code .gitlab-ci.yml
- of the dependent project, include the following snippet.
- The project will rebuild at the end of the build.
+ %p.light
+ Copy one of the tokens above, set your branch or tag name, and that
+ reference will be rebuilt.
- %pre
- :plain
- trigger:
- type: deploy
- script:
- - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
- %h5.prepend-top-default
- Pass build variables
+ %pre
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ -F ref=REF_NAME \
+ #{builds_trigger_url(@project.id)}
+ %h5.prepend-top-default
+ Use .gitlab-ci.yml
- %p.light
- Add
- %code variables[VARIABLE]=VALUE
- to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
+ %p.light
+ In the
+ %code .gitlab-ci.yml
+ of another project, include the following snippet.
+ The project will be rebuilt at the end of the build.
- %pre.append-bottom-0
- :plain
- curl -X POST \
- -F token=TOKEN \
- -F "ref=REF_NAME" \
- -F "variables[RUN_NIGHTLY_BUILD]=true" \
- #{builds_trigger_url(@project.id)}
+ %pre
+ :plain
+ trigger_build:
+ stage: deploy
+ script:
+ - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
+ %h5.prepend-top-default
+ Pass build variables
+
+ %p.light
+ Add
+ %code variables[VARIABLE]=VALUE
+ to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
+
+ %pre.append-bottom-0
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ -F "ref=REF_NAME" \
+ -F "variables[RUN_NIGHTLY_BUILD]=true" \
+ #{builds_trigger_url(@project.id)}
diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml
index 6c43f822db4..07cee86ba4c 100644
--- a/app/views/projects/variables/_table.html.haml
+++ b/app/views/projects/variables/_table.html.haml
@@ -9,7 +9,7 @@
%th Value
%th
%tbody
- - @project.variables.each do |variable|
+ - @project.variables.order_key_asc.each do |variable|
- if variable.id?
%tr
%td= variable.key
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 643f7c589e6..6624d5cb427 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -24,7 +24,7 @@
= succeed '.' do
More examples are in the
- = link_to 'documentation', help_page_path("user/project/markdown", anchor: "wiki-specific-markdown")
+ = link_to 'documentation', help_page_path("user/markdown", anchor: "wiki-specific-markdown")
.form-group
= f.label :commit_message, class: 'control-label'
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index 551a20c1044..09c4411d67e 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,15 +1,16 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
- = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = 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#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
+ = nav_link(path: 'wikis#git_access') do
+ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
+ Git Access
- = render 'projects/wikis/new'
+ = render 'projects/wikis/new'
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 252c37532e1..7fe2bce3e7c 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -10,12 +10,16 @@
in group #{link_to @group.name, @group}
.results.prepend-top-10
- .search-results
- - if @scope == 'projects'
- .term
- = render 'shared/projects/list', projects: @search_objects
- - else
- = render partial: "search/results/#{@scope.singularize}", collection: @search_objects
+ - if @scope == 'commits'
+ %ul.list-unstyled
+ = render partial: "search/results/commit", collection: @search_objects
+ - else
+ .search-results
+ - if @scope == 'projects'
+ .term
+ = render 'shared/projects/list', projects: @search_objects
+ - else
+ = render partial: "search/results/#{@scope.singularize}", collection: @search_objects
- if @scope != 'projects'
= paginate(@search_objects, theme: 'gitlab')
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index 290743feb4a..6f0a0ea36ec 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,4 +1,4 @@
-- blob = @project.repository.parse_search_result(blob)
+- blob = parse_search_result(blob)
.blob-result
.file-holder
.file-title
diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml
index 4e6c3965dc6..5b2d83d6b92 100644
--- a/app/views/search/results/_commit.html.haml
+++ b/app/views/search/results/_commit.html.haml
@@ -1,2 +1 @@
-.search-result-row
- = render 'projects/commits/commit', project: @project, commit: commit
+= render 'projects/commits/commit', project: @project, commit: commit
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index 235106c4f74..648d0bd76cb 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -1,4 +1,4 @@
-- wiki_blob = @project.repository.parse_search_result(wiki_blob)
+- wiki_blob = parse_search_result(wiki_blob)
.blob-result
.file-holder
.file-title
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
new file mode 100644
index 00000000000..9ce6a1aeef5
--- /dev/null
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -0,0 +1,19 @@
+- noteable = @sent_notification.noteable
+- noteable_type = @sent_notification.noteable_type.humanize(capitalize: false)
+- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
+
+- page_title "Unsubscribe", noteable_text, @sent_notification.noteable_type.humanize.pluralize, @sent_notification.project.name_with_namespace
+
+
+%h3.page-title
+ Unsubscribe from #{noteable_type} #{noteable_text}
+
+%p
+ = succeed '?' do
+ Are you sure you want to unsubscribe from #{noteable_type}
+ = link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable])
+
+%p
+ = link_to 'Unsubscribe', unsubscribe_sent_notification_path(@sent_notification, force: true),
+ class: 'btn btn-primary append-right-10'
+ = link_to 'Cancel', new_user_session_path, class: 'btn append-right-10'
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index cf16c203f9c..73d288e2236 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,10 +1,19 @@
+- if @project
+ - counts = milestone_counts(@project.milestones)
+
%ul.nav-links
- %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
+ %li{class: milestone_class_for_state(params[:state], 'opened', true)}
= link_to milestones_filter_path(state: 'opened') do
Open
- %li{class: ("active" if params[:state] == 'closed')}
+ - if @project
+ %span.badge #{counts[:opened]}
+ %li{class: milestone_class_for_state(params[:state], 'closed')}
= link_to milestones_filter_path(state: 'closed') do
Closed
- %li{class: ("active" if params[:state] == 'all')}
+ - if @project
+ %span.badge #{counts[:closed]}
+ %li{class: milestone_class_for_state(params[:state], 'all')}
= link_to milestones_filter_path(state: 'all') do
All
+ - if @project
+ %span.badge #{counts[:all]}
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 249bce926ce..36bbac6fbf5 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -8,26 +8,26 @@
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
- = link_to page_filter_path(sort: sort_value_priority) do
+ = link_to page_filter_path(sort: sort_value_priority, label: true) do
= sort_title_priority
- = link_to page_filter_path(sort: sort_value_recently_created) do
+ = link_to page_filter_path(sort: sort_value_recently_created, label: true) do
= sort_title_recently_created
- = link_to page_filter_path(sort: sort_value_oldest_created) do
+ = link_to page_filter_path(sort: sort_value_oldest_created, label: true) do
= sort_title_oldest_created
- = link_to page_filter_path(sort: sort_value_recently_updated) do
+ = link_to page_filter_path(sort: sort_value_recently_updated, label: true) do
= sort_title_recently_updated
- = link_to page_filter_path(sort: sort_value_oldest_updated) do
+ = link_to page_filter_path(sort: sort_value_oldest_updated, label: true) do
= sort_title_oldest_updated
- = link_to page_filter_path(sort: sort_value_milestone_soon) do
+ = link_to page_filter_path(sort: sort_value_milestone_soon, label: true) do
= sort_title_milestone_soon
- = link_to page_filter_path(sort: sort_value_milestone_later) do
+ = link_to page_filter_path(sort: sort_value_milestone_later, label: true) do
= sort_title_milestone_later
- if controller.controller_name == 'issues' || controller.action_name == 'issues'
- = link_to page_filter_path(sort: sort_value_due_date_soon) do
+ = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do
= sort_title_due_date_soon
- = link_to page_filter_path(sort: sort_value_due_date_later) do
+ = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do
= sort_title_due_date_later
- = link_to page_filter_path(sort: sort_value_upvotes) do
+ = link_to page_filter_path(sort: sort_value_upvotes, label: true) do
= sort_title_upvotes
- = link_to page_filter_path(sort: sort_value_downvotes) do
+ = link_to page_filter_path(sort: sort_value_downvotes, label: true) do
= sort_title_downvotes
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index 107ad19177c..b11257ee0e6 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -1,12 +1,12 @@
.form-group.project-visibility-level-holder
= f.label :visibility_level, class: 'control-label' do
Visibility Level
- = link_to "(?)", help_page_path("public_access/public_access")
+ = link_to icon('question-circle'), help_page_path("public_access/public_access")
.col-sm-10
- if can_change_visibility_level
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
- else
- .col-sm-10
+ %div
%span.info
= visibility_level_icon(visibility_level)
%strong
diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml
index ebe2eb0433d..182c4eebd50 100644
--- a/app/views/shared/_visibility_radios.html.haml
+++ b/app/views/shared/_visibility_radios.html.haml
@@ -10,6 +10,6 @@
.option-descr
= visibility_level_description(level, form_model)
- unless restricted_visibility_levels.empty?
- .col-sm-10
+ %div
%span.info
Some visibility level settings have been restricted by the administrator.
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
new file mode 100644
index 00000000000..60353aee7f1
--- /dev/null
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -0,0 +1,24 @@
+%ul.nav-links
+ %li{ class: ('active' if scope.nil?) }
+ = link_to build_path_proc.call(nil) do
+ All
+ %span.badge.js-totalbuilds-count
+ = number_with_delimiter(all_builds.count(:id))
+
+ %li{ class: ('active' if scope == 'pending') }
+ = link_to build_path_proc.call('pending') do
+ Pending
+ %span.badge
+ = number_with_delimiter(all_builds.pending.count(:id))
+
+ %li{ class: ('active' if scope == 'running') }
+ = link_to build_path_proc.call('running') do
+ Running
+ %span.badge
+ = number_with_delimiter(all_builds.running.count(:id))
+
+ %li{ class: ('active' if scope == 'finished') }
+ = link_to build_path_proc.call('finished') do
+ Finished
+ %span.badge
+ = number_with_delimiter(all_builds.finished.count(:id))
diff --git a/app/views/shared/icons/_icon_cycle_analytics_splash.svg b/app/views/shared/icons/_icon_cycle_analytics_splash.svg
new file mode 100644
index 00000000000..eb5a962d651
--- /dev/null
+++ b/app/views/shared/icons/_icon_cycle_analytics_splash.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 99 102" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m35.12 56.988c4.083-4.385 5.968-12.155 5.968-24.04 0-20.2-15.874-32.16-15.874-32.16-1.114-.954-2.929-.979-4.04 0 0 0-15.874 11.957-15.874 32.16 0 11.882 1.884 19.652 5.968 24.04h23.848"/><mask id="1" width="35.783" height="56.924" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(0-4)"><g transform="translate(32.15 3.976)"><g fill="#6b4fbb"><path d="m11.928 56.988l1.325-1.325v3.313c0 .737.59 1.325 1.325 1.325h17.229c.736 0 1.325-.59 1.325-1.325v-3.313l1.325 1.325h-22.53m22.53-1.325v3.313c0 1.464-1.18 2.651-2.651 2.651h-17.229c-1.464 0-2.651-1.178-2.651-2.651v-3.313h22.53m-5.964 7.361h.663c0 3.294-2.67 5.964-5.964 5.964-3.294 0-5.964-2.67-5.964-5.964h.663.663c0 2.562 2.077 4.639 4.639 4.639 2.562 0 4.639-2.077 4.639-4.639h.663"/><path d="m5.816 42.535c-.346-2.839-.515-6.03-.515-9.584 0-20.2 15.874-32.16 15.874-32.16 1.106-.979 2.921-.954 4.04 0 0 0 15.874 11.957 15.874 32.16 0 11.882-1.884 19.652-5.968 24.04h-23.848c-2.861-3.073-4.643-7.807-5.453-14.453-.06-.493-.115-.997-.164-1.511l-4.04 2.884c-.891.637-1.614 2.041-1.614 3.137v14.581c0 1.465.971 1.958 2.165 1.106l8.691-6.208c-.282-.332-.553-.681-.813-1.048l-8.648 6.177c-.147.105-.069.152-.069-.027v-14.581c0-.668.516-1.671 1.059-2.059l3.432-2.451m38.4 20.2c1.193.852 2.165.359 2.165-1.106v-14.581c0-1.096-.723-2.5-1.614-3.137l-4.04-2.884c-.049.514-.104 1.018-.164 1.511l3.432 2.451c.543.388 1.059 1.391 1.059 2.059v14.581c0 .179.078.132-.069.027l-8.648-6.177c-.26.367-.531.716-.813 1.048l8.691 6.208"/></g><use fill="#fff" stroke="#6b4fbb" stroke-width="2.651" mask="url(#1)" xlink:href="#0"/><g fill="#b5a7dd"><path d="m30.482 28.494c0-4.03-3.263-7.289-7.289-7.289-4.03 0-7.289 3.263-7.289 7.289 0 4.03 3.263 7.289 7.289 7.289 4.03 0 7.289-3.263 7.289-7.289m-15.904 0c0-4.758 3.857-8.614 8.614-8.614 4.758 0 8.614 3.857 8.614 8.614 0 4.758-3.857 8.614-8.614 8.614-4.758 0-8.614-3.857-8.614-8.614"/><path d="m27.17 28.494c0-2.196-1.78-3.976-3.976-3.976-2.196 0-3.976 1.78-3.976 3.976 0 2.196 1.78 3.976 3.976 3.976 2.196 0 3.976-1.78 3.976-3.976m-9.277 0c0-2.928 2.373-5.301 5.301-5.301 2.928 0 5.301 2.373 5.301 5.301 0 2.928-2.373 5.301-5.301 5.301-2.928 0-5.301-2.373-5.301-5.301"/></g><path fill="#6b4fbb" d="m34.458 87.47c0 1.098.89 1.988 1.988 1.988 1.098 0 1.988-.89 1.988-1.988 0-.366.297-.663.663-.663.366 0 .663.297.663.663 0 1.83-1.483 3.313-3.313 3.313-1.826 0-3.307-1.478-3.313-3.302 0-.002 0-.003 0-.005v-2.663c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.657m-21.2-6.615c0-.002 0-.003 0-.005v-2.663c0-.358-.297-.657-.663-.657-.369 0-.663.294-.663.657v2.657c0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 1.83 1.483 3.313 3.313 3.313 1.826 0 3.307-1.477 3.313-3.302m5.301 7.285c0-.001 0-.002 0-.003v-16.576c0-.362-.297-.658-.663-.658-.369 0-.663.295-.663.658v16.571c0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 2.745 2.225 4.97 4.97 4.97 2.742 0 4.966-2.221 4.97-4.963m10.602 8.607v-18.555c0-.365-.297-.661-.663-.661-.369 0-.663.296-.663.661v18.557c0 0 0 0 0 .001.001 2.744 2.226 4.968 4.97 4.968 2.745 0 4.97-2.225 4.97-4.97 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645m3.976-25.19c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m0 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-10.602-6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m5.301 0c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-5.301 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m0 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-10.602-13.253c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663"/></g><path fill="#e2ddf2" d="m97.75 76.54c0-2.745-2.225-4.97-4.97-4.97-2.745 0-4.97 2.225-4.97 4.97 0 2.745 2.225 4.97 4.97 4.97 2.745 0 4.97-2.225 4.97-4.97m-8.614 0c0-2.01 1.632-3.645 3.645-3.645 2.01 0 3.645 1.632 3.645 3.645 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645m-60.964-57.651c0-2.745-2.225-4.97-4.97-4.97-2.745 0-4.97 2.225-4.97 4.97 0 2.745 2.225 4.97 4.97 4.97 2.745 0 4.97-2.225 4.97-4.97m-8.614 0c0-2.01 1.632-3.645 3.645-3.645 2.01 0 3.645 1.632 3.645 3.645 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645"/><path fill="#b5a7dd" d="m98.41 34.458c0-1.83-1.483-3.313-3.313-3.313-1.83 0-3.313 1.483-3.313 3.313 0 1.83 1.483 3.313 3.313 3.313 1.83 0 3.313-1.483 3.313-3.313m-5.301 0c0-1.098.89-1.988 1.988-1.988 1.098 0 1.988.89 1.988 1.988 0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988m-86.14 20.542c0-1.83-1.483-3.313-3.313-3.313-1.83 0-3.313 1.483-3.313 3.313 0 1.83 1.483 3.313 3.313 3.313 1.83 0 3.313-1.483 3.313-3.313m-5.301 0c0-1.098.89-1.988 1.988-1.988 1.098 0 1.988.89 1.988 1.988 0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988"/></g></svg>
diff --git a/app/views/shared/icons/_icon_fork.svg b/app/views/shared/icons/_icon_fork.svg
index fc970e4ce50..ce22b6cdaea 100644
--- a/app/views/shared/icons/_icon_fork.svg
+++ b/app/views/shared/icons/_icon_fork.svg
@@ -1,3 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="30" height="40" viewBox="5 0 30 40">
- <path fill="#7E7E7E" fill-rule="evenodd" d="M22,29.5351288 L22,22.7193602 C26.1888699,21.5098039 29.3985457,16.802989 29.3985457,16.802989 C29.740988,16.3567547 30,15.5559546 30,15.0081969 L30,10.4648712 C31.1956027,9.77325238 32,8.48056471 32,7 C32,4.790861 30.209139,3 28,3 C25.790861,3 24,4.790861 24,7 C24,8.48056471 24.8043973,9.77325238 26,10.4648712 L26,14.7083871 C26,14.8784435 25.9055559,15.0987329 25.7890533,15.2104147 C25.7890533,15.2104147 24.5373893,16.4126202 23.9488702,16.9515733 C22.5015398,18.2770075 21.1191354,19 20.090554,19 C19.0477772,19 17.6172728,18.2608988 16.1128852,16.9142923 C15.5030182,16.3683886 14.3672121,15.3403307 14.3672121,15.3403307 C14.1659605,15.1583364 14.0000086,14.7846305 14.0000192,14.5088473 C14.0000192,14.5088473 14.0000932,12.7539451 14.0001308,10.4647956 C15.1956614,9.77315812 16,8.48051074 16,7 C16,4.790861 14.209139,3 12,3 C9.790861,3 8,4.790861 8,7 C8,8.48056471 8.80439726,9.77325238 10,10.4648712 L10,15.0081969 C10,15.5446944 10.2736352,16.3534183 10.6111812,16.7893819 C10.6111812,16.7893819 13.8599776,21.3779363 18,22.6668724 L18,29.5351288 C16.8043973,30.2267476 16,31.5194353 16,33 C16,35.209139 17.790861,37 20,37 C22.209139,37 24,35.209139 24,33 C24,31.5194353 23.1956027,30.2267476 22,29.5351288 Z M14,7 C14,5.8954305 13.1045695,5 12,5 C10.8954305,5 10,5.8954305 10,7 C10,8.1045695 10.8954305,9 12,9 C13.1045695,9 14,8.1045695 14,7 Z M30,7 C30,5.8954305 29.1045695,5 28,5 C26.8954305,5 26,5.8954305 26,7 C26,8.1045695 26.8954305,9 28,9 C29.1045695,9 30,8.1045695 30,7 Z M22,33 C22,31.8954305 21.1045695,31 20,31 C18.8954305,31 18,31.8954305 18,33 C18,34.1045695 18.8954305,35 20,35 C21.1045695,35 22,34.1045695 22,33 Z"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="40" viewBox="5 0 30 40"><path fill="#7E7E7E" fill-rule="evenodd" d="M22 29.535V22.72c4.19-1.21 7.4-5.917 7.4-5.917.34-.446.6-1.247.6-1.795v-4.543C31.196 9.773 32 8.48 32 7c0-2.21-1.79-4-4-4s-4 1.79-4 4c0 1.48.804 2.773 2 3.465v4.243c0 .17-.094.39-.21.502 0 0-1.253 1.203-1.84 1.742C22.5 18.277 21.12 19 20.09 19c-1.042 0-2.473-.74-3.977-2.086-.61-.546-1.746-1.574-1.746-1.574-.2-.182-.367-.555-.367-.83v-4.045C15.196 9.773 16 8.48 16 7c0-2.21-1.79-4-4-4S8 4.79 8 7c0 1.48.804 2.773 2 3.465v4.543c0 .537.274 1.345.61 1.78 0 0 3.25 4.59 7.39 5.88v6.867c-1.196.692-2 1.984-2 3.465 0 2.21 1.79 4 4 4s4-1.79 4-4c0-1.48-.804-2.773-2-3.465zM14 7c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2zm16 0c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2zm-8 26c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2z"/></svg>
diff --git a/app/views/shared/icons/_icon_no_wrap.svg b/app/views/shared/icons/_icon_no_wrap.svg
new file mode 100644
index 00000000000..fe34cada002
--- /dev/null
+++ b/app/views/shared/icons/_icon_no_wrap.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+ <path fill-rule="evenodd" d="m6 11h-4.509c-.263 0-.491.226-.491.505v.991c0 .291.22.505.491.505h4.509v.679c0 .301.194.413.454.236l2.355-1.607c.251-.171.259-.442 0-.619l-2.355-1.607c-.251-.171-.454-.07-.454.236v.681m-5-7.495c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991m10 8c0-.279.215-.505.49-.505h3.02c.271 0 .49.214.49.505v.991c0 .279-.215.505-.49.505h-3.02c-.271 0-.49-.214-.49-.505v-.991m-10-4c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991"/>
+</svg>
diff --git a/app/views/shared/icons/_icon_soft_wrap.svg b/app/views/shared/icons/_icon_soft_wrap.svg
new file mode 100644
index 00000000000..ea27a2024b1
--- /dev/null
+++ b/app/views/shared/icons/_icon_soft_wrap.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+ <path fill-rule="evenodd" d="m12 11h-2v-.681c0-.307-.203-.407-.454-.236l-2.355 1.607c-.259.177-.251.448 0 .619l2.355 1.607c.259.177.454.065.454-.236v-.679h2c0 0 0 0 0 0 1.657 0 3-1.343 3-3 0-.828-.336-1.578-.879-2.121-.543-.543-1.293-.879-2.121-.879-.001 0-.002 0-.002 0h-10.497c-.271 0-.5.226-.5.505v.991c0 .291.224.505.5.505h10.497c.001 0 .002 0 .002 0 .552 0 1 .448 1 1 0 .276-.112.526-.293.707-.181.181-.431.293-.707.293m-11-7.495c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991m0 8c0-.279.215-.505.49-.505h3.02c.271 0 .49.214.49.505v.991c0 .279-.215.505-.49.505h-3.02c-.271 0-.49-.214-.49-.505v-.991"/>
+</svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index a3bad0e8326..6eafc309a13 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,10 +1,12 @@
- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder
+- boards_page = controller.controller_name == 'boards'
+
.issues-filters
.issues-details-filters.row-content-block.second-block
- = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search]), method: :get, class: 'filter-form js-filter-form' do
- - if params[:issue_search].present?
- = hidden_field_tag :issue_search, params[:issue_search]
- - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
+ = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
+ - if params[:search].present?
+ = hidden_field_tag :search, params[:search]
+ - if @bulk_edit
.check-all-holder
= check_box_tag "check_all_issues", nil, false,
class: "check_all_issues left"
@@ -27,8 +29,11 @@
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
+ .filter-item.inline.reset-filters
+ %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters
+
.pull-right
- - if controller.controller_name == 'boards'
+ - if boards_page
#js-boards-seach.issue-boards-search
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project)
@@ -43,9 +48,9 @@
- else
= render 'shared/sort_dropdown'
- - if controller.controller_name == 'issues'
+ - if @bulk_edit
.issues_bulk_update.hide
- = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post, class: 'bulk-update' do
+ = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
%ul
@@ -68,10 +73,10 @@
%li
%a{href: "#", data: {id: "unsubscribe"}} Unsubscribe
- = hidden_field_tag 'update[issues_ids]', []
+ = hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
- = button_tag "Update issues", class: "btn update_selected_issues btn-save"
+ = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
- if !@labels.nil?
.row-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) }
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index ac593d8937a..1c2324e7f75 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -20,7 +20,7 @@
= dropdown_tag(title, options: { toggle_class: 'js-issuable-selector',
title: title, filter: true, placeholder: 'Filter', footer_content: true,
- data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: @project.path, namespace_path: @project.namespace.path } } ) do
+ data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: ref_project.path, namespace_path: ref_project.namespace.path } } ) do
%ul.dropdown-footer-list
%li
%a.reset-template
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 6d5bdbe435e..d537fd6712a 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -16,6 +16,7 @@
- if selected
- selected.each do |label|
= hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : u(label.try(:title)), id: nil
+
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
%span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) }
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 1d9b09a5ef1..5527a2f889a 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -1,25 +1,25 @@
+- type = local_assigns.fetch(:type, :issues)
+- page_context_word = type.to_s.humanize(capitalize: false)
+- issuables = @issues || @merge_requests
+
%ul.nav-links.issues-state-filters
- - if defined?(type) && type == :merge_requests
- - page_context_word = 'merge requests'
- - else
- - page_context_word = 'issues'
%li{class: ("active" if params[:state] == 'opened')}
= link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do
- #{state_filters_text_for(:opened, @project)}
+ #{issuables_state_counter_text(type, :opened)}
- - if defined?(type) && type == :merge_requests
+ - if type == :merge_requests
%li{class: ("active" if params[:state] == 'merged')}
= link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do
- #{state_filters_text_for(:merged, @project)}
+ #{issuables_state_counter_text(type, :merged)}
%li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do
- #{state_filters_text_for(:closed, @project)}
+ #{issuables_state_counter_text(type, :closed)}
- else
%li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do
- #{state_filters_text_for(:closed, @project)}
+ #{issuables_state_counter_text(type, :closed)}
%li{class: ("active" if params[:state] == 'all')}
= link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do
- #{state_filters_text_for(:all, @project)}
+ #{issuables_state_counter_text(type, :all)}
diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml
index 186963b32b8..2c89217cadd 100644
--- a/app/views/shared/issuable/_search_form.html.haml
+++ b/app/views/shared/issuable/_search_form.html.haml
@@ -1,2 +1,2 @@
-= form_tag(path, method: :get, id: "issue_search_form", class: 'issue-search-form') do
- = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by name ...', class: 'form-control issue_search search-text-input input-short', spellcheck: false }
+= form_tag(path, method: :get, id: "issuable_search_form", class: 'issuable-search-form') do
+ = search_field_tag :search, params[:search], { id: 'issuable_search', placeholder: 'Filter by name ...', class: 'form-control issuable_search search-text-input input-short', spellcheck: false }
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index acc3ccf4dcf..3dccfb147bf 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -33,7 +33,7 @@
- if @project
.row
.col-sm-6= render('shared/milestone_expired', milestone: milestone)
- .col-sm-6
+ .col-sm-6.milestone-actions
- 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 btn-grouped" do
Edit
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index af753496260..7ae4211ddfd 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -6,12 +6,13 @@
%strong.item-title
Snippet #{@snippet.to_reference}
%span.creator
- created by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title")}
+ authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- if @snippet.updated_at != @snippet.created_at
%span
= icon('edit', title: 'edited')
= time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago')
+ by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
.snippet-actions
- if @snippet.project_id?
@@ -19,6 +20,5 @@
- else
= render "snippets/actions"
-.content-block.second-block
- %h2.snippet-title.prepend-top-0.append-bottom-0
- = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author
+%h2.snippet-title.prepend-top-0.append-bottom-0
+ = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index c96dfefe17f..ea17bec8677 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -3,19 +3,30 @@
.title
= link_to reliable_snippet_path(snippet) do
- = truncate(snippet.title, length: 60)
+ = snippet.title
- if snippet.private?
- %span.label.label-gray
+ %span.label.label-gray.hidden-xs
= icon('lock')
private
- %span.monospace.pull-right
+ %span.monospace.pull-right.hidden-xs
= snippet.file_name
- %small.pull-right.cgray
+ %ul.controls.visible-xs
+ %li
+ - note_count = snippet.notes.user.count
+ = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
+ = icon('comments')
+ = note_count
+ %li
+ %span.sr-only
+ = visibility_level_label(snippet.visibility_level)
+ = visibility_level_icon(snippet.visibility_level, fw: false)
+
+ %small.pull-right.cgray.hidden-xs
- if snippet.project_id?
= link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project)
- .snippet-info
+ .snippet-info.hidden-xs
= link_to user_snippets_path(snippet.author) do
= snippet.author_name
authored #{time_ago_with_tooltip(snippet.created_at)}
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index d2ec6c3ddef..5d659eb83a9 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -52,6 +52,13 @@
%p.light
This URL will be triggered when an issue is created/updated/merged
%li
+ = f.check_box :confidential_issues_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :confidential_issues_events, class: 'list-label' do
+ %strong Confidential Issues events
+ %p.light
+ This URL will be triggered when a confidential issue is created/updated/merged
+ %li
= f.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 160c6cd84da..c446dc3bdc1 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,13 +1,13 @@
.hidden-xs
- if current_user
- = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New Snippet" do
- New Snippet
- - if can?(current_user, :update_personal_snippet, @snippet)
- = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
- Edit
+ = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do
+ New snippet
- 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-danger", title: 'Delete Snippet' do
Delete
+ - if can?(current_user, :update_personal_snippet, @snippet)
+ = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
+ Edit
- if current_user
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
@@ -16,13 +16,13 @@
.dropdown-menu.dropdown-menu-full-width
%ul
%li
- = link_to new_snippet_path, title: "New Snippet" do
- New Snippet
- - if can?(current_user, :update_personal_snippet, @snippet)
- %li
- = link_to edit_snippet_path(@snippet) do
- Edit
+ = link_to new_snippet_path, title: "New snippet" do
+ New snippet
- if can?(current_user, :admin_personal_snippet, @snippet)
%li
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
Delete
+ - if can?(current_user, :update_personal_snippet, @snippet)
+ %li
+ = link_to edit_snippet_path(@snippet) do
+ Edit
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index 80a3e731e1d..77b66ca74b6 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,7 +1,13 @@
-%ul.content-list
- = render partial: 'shared/snippets/snippet', collection: @snippets
- - if @snippets.empty?
- %li
- .nothing-here-block Nothing here.
+- remote = local_assigns.fetch(:remote, false)
-= paginate @snippets, theme: 'gitlab'
+.snippets-list-holder
+ %ul.content-list
+ = render partial: 'shared/snippets/snippet', collection: @snippets
+ - if @snippets.empty?
+ %li
+ .nothing-here-block Nothing here.
+
+ = paginate @snippets, theme: 'gitlab', remote: remote
+
+:javascript
+ gl.SnippetsList();
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index ed3992650d4..cd89155c616 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,13 +1,14 @@
- page_title @snippet.title, "Snippets"
-.snippet-holder
- = render 'shared/snippets/header'
+= render 'shared/snippets/header'
- %article.file-holder.file-holder-no-border.snippet-file-content
- .file-title.file-title-clear
- = blob_icon 0, @snippet.file_name
- = @snippet.file_name
- .file-actions.hidden-xs
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
- = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
- = render 'shared/snippets/blob'
+%article.file-holder.snippet-file-content
+ .file-title
+ = blob_icon 0, @snippet.file_name
+ = @snippet.file_name
+ .file-actions
+ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+ = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
+ = render 'shared/snippets/blob'
+
+= render 'award_emoji/awards_block', awardable: @snippet, inline: true \ No newline at end of file
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index 75fb0e303ad..9657101ace5 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -20,6 +20,8 @@
%div
%p We heard back from your U2F device. Click this button to authenticate with the GitLab server.
= form_tag(new_user_session_path, method: :post) do |f|
+ - resource_params = params[resource_name].presence || params
+ = hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0)
= hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
= submit_tag "Authenticate via U2F Device", class: "btn btn-success"
diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml
index 77f2ddefb1e..09ff8a76d27 100644
--- a/app/views/users/calendar.html.haml
+++ b/app/views/users/calendar.html.haml
@@ -4,6 +4,6 @@
Summary of issues, merge requests, and push events
:javascript
new Calendar(
- #{@timestamps.to_json},
+ #{@activity_dates.to_json},
'#{user_calendar_activities_path}'
- );
+ ); \ No newline at end of file
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 9a052abe40a..60fc0c0daf6 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -10,72 +10,76 @@
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
.user-profile
- .cover-block
+ .cover-block.user-cover-block
.cover-controls
- if @user == current_user
= link_to profile_path, class: 'btn btn-gray' do
= icon('pencil')
- elsif current_user
- %span.report-abuse
- - if @user.abuse_report
- %button.btn.btn-danger{ title: 'Already reported for abuse',
- data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
- = icon('exclamation-circle')
- - else
- = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
- title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
- = icon('exclamation-circle')
+ - if @user.abuse_report
+ %button.btn.btn-danger{ title: 'Already reported for abuse',
+ data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }}
+ = icon('exclamation-circle')
+ - else
+ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
+ title: 'Report abuse', data: {toggle: 'tooltip', placement: 'bottom', 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.admin?
- &nbsp;
= link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('users')
- .avatar-holder
- = link_to avatar_icon(@user, 400), target: '_blank' do
- = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
- .cover-title
- = @user.name
-
- .cover-desc
- %span.middle-dot-divider
- @#{@user.username}
- %span.middle-dot-divider
- Member since #{@user.created_at.to_s(:medium)}
+ .profile-header
+ .avatar-holder
+ = link_to avatar_icon(@user, 400), target: '_blank' do
+ = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
+
+ .user-info
+ .cover-title
+ = @user.name
+ %span.handle
+ @#{@user.username}
+
+ .cover-desc.member-date
+ %span.middle-dot-divider
+ Member since #{@user.created_at.to_s(:medium)}
+
+ .cover-desc
+ - unless @user.public_email.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.public_email, "mailto:#{@user.public_email}"
+ - unless @user.skype.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "skype:#{@user.skype}", title: "Skype" do
+ = icon('skype')
+ - unless @user.linkedin.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
+ = icon('linkedin-square')
+ - unless @user.twitter.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
+ = icon('twitter-square')
+ - unless @user.website_url.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.short_website_url, @user.full_website_url
+ - unless @user.location.blank?
+ .profile-link-holder.middle-dot-divider
+ = icon('map-marker')
+ = @user.location
+ - unless @user.organization.blank?
+ .profile-link-holder.middle-dot-divider
+ = icon('building')
+ = @user.organization
- if @user.bio.present?
.cover-desc
%p.profile-user-bio
= @user.bio
- .cover-desc
- - unless @user.public_email.blank?
- .profile-link-holder.middle-dot-divider
- = link_to @user.public_email, "mailto:#{@user.public_email}"
- - unless @user.skype.blank?
- .profile-link-holder.middle-dot-divider
- = link_to "skype:#{@user.skype}", title: "Skype" do
- = icon('skype')
- - unless @user.linkedin.blank?
- .profile-link-holder.middle-dot-divider
- = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
- = icon('linkedin-square')
- - unless @user.twitter.blank?
- .profile-link-holder.middle-dot-divider
- = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
- = icon('twitter-square')
- - unless @user.website_url.blank?
- .profile-link-holder.middle-dot-divider
- = link_to @user.short_website_url, @user.full_website_url
- - unless @user.location.blank?
- .profile-link-holder.middle-dot-divider
- = icon('map-marker')
- = @user.location
-
%ul.nav-links.center.user-profile-nav
%li.js-activity-tab
= link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
new file mode 100644
index 00000000000..5883cafe1d1
--- /dev/null
+++ b/app/workers/prune_old_events_worker.rb
@@ -0,0 +1,17 @@
+class PruneOldEventsWorker
+ include Sidekiq::Worker
+
+ def perform
+ # Contribution calendar shows maximum 12 months of events.
+ # Double nested query is used because MySQL doesn't allow DELETE subqueries
+ # on the same table.
+ Event.unscoped.where(
+ '(id IN (SELECT id FROM (?) ids_to_remove))',
+ Event.unscoped.where(
+ 'created_at < ?',
+ (12.months + 1.day).ago).
+ select(:id).
+ limit(10_000)).
+ delete_all
+ end
+end
diff --git a/changelogs/archive.md b/changelogs/archive.md
new file mode 100644
index 00000000000..c68ab694d39
--- /dev/null
+++ b/changelogs/archive.md
@@ -0,0 +1,1810 @@
+## 7.14.3
+
+- No changes
+
+## 7.14.2
+
+- Upgrade gitlab_git to 7.2.15 to fix `git blame` errors with ISO-encoded files (Stan Hu)
+- Allow configuration of LDAP attributes GitLab will use for the new user account.
+
+## 7.14.1
+
+- Improve abuse reports management from admin area
+- Fix "Reload with full diff" URL button in compare branch view (Stan Hu)
+- Disabled DNS lookups for SSH in docker image (Rowan Wookey)
+- Only include base URL in OmniAuth full_host parameter (Stan Hu)
+- Fix Error 500 in API when accessing a group that has an avatar (Stan Hu)
+- Ability to enable SSL verification for Webhooks
+
+## 7.14.0
+
+- Fix bug where non-project members of the target project could set labels on new merge requests.
+- Update default robots.txt rules to disallow crawling of irrelevant pages (Ben Bodenmiller)
+- Fix redirection after sign in when using auto_sign_in_with_provider
+- Upgrade gitlab_git to 7.2.14 to ignore CRLFs in .gitmodules (Stan Hu)
+- Clear cache to prevent listing deleted branches after MR removes source branch (Stan Hu)
+- Provide more feedback what went wrong if HipChat service failed test (Stan Hu)
+- Fix bug where backslashes in inline diffs could be dropped (Stan Hu)
+- Disable turbolinks when linking to Bitbucket import status (Stan Hu)
+- Fix broken code import and display error messages if something went wrong with creating project (Stan Hu)
+- Fix corrupted binary files when using API files endpoint (Stan Hu)
+- Bump Haml to 4.0.7 to speed up textarea rendering (Stan Hu)
+- Show incompatible projects in Bitbucket import status (Stan Hu)
+- Fix coloring of diffs on MR Discussion-tab (Gert Goet)
+- Fix "Network" and "Graphs" pages for branches with encoded slashes (Stan Hu)
+- Fix errors deleting and creating branches with encoded slashes (Stan Hu)
+- Always add current user to autocomplete controller to support filter by "Me" (Stan Hu)
+- Fix multi-line syntax highlighting (Stan Hu)
+- Fix network graph when branch name has single quotes (Stan Hu)
+- Add "Confirm user" button in user admin page (Stan Hu)
+- Upgrade gitlab_git to version 7.2.6 to fix Error 500 when creating network graphs (Stan Hu)
+- Add support for Unicode filenames in relative links (Hiroyuki Sato)
+- Fix URL used for refreshing notes if relative_url is present (Bartłomiej Święcki)
+- Fix commit data retrieval when branch name has single quotes (Stan Hu)
+- Check that project was actually created rather than just validated in import:repos task (Stan Hu)
+- Fix full screen mode for snippet comments (Daniel Gerhardt)
+- Fix 404 error in files view after deleting the last file in a repository (Stan Hu)
+- Fix the "Reload with full diff" URL button (Stan Hu)
+- Fix label read access for unauthenticated users (Daniel Gerhardt)
+- Fix access to disabled features for unauthenticated users (Daniel Gerhardt)
+- Fix OAuth provider bug where GitLab would not go return to the redirect_uri after sign-in (Stan Hu)
+- Fix file upload dialog for comment editing (Daniel Gerhardt)
+- Set OmniAuth full_host parameter to ensure redirect URIs are correct (Stan Hu)
+- Return comments in created order in merge request API (Stan Hu)
+- Disable internal issue tracker controller if external tracker is used (Stan Hu)
+- Expire Rails cache entries after two weeks to prevent endless Redis growth
+- Add support for destroying project milestones (Stan Hu)
+- Allow custom backup archive permissions
+- Add project star and fork count, group avatar URL and user/group web URL attributes to API
+- Show who last edited a comment if it wasn't the original author
+- Send notification to all participants when MR is merged.
+- Add ability to manage user email addresses via the API.
+- Show buttons to add license, changelog and contribution guide if they're missing.
+- Tweak project page buttons.
+- Disabled autocapitalize and autocorrect on login field (Daryl Chan)
+- Mention group and project name in creation, update and deletion notices (Achilleas Pipinellis)
+- Update gravatar link on profile page to link to configured gravatar host (Ben Bodenmiller)
+- Remove redis-store TTL monkey patch
+- Add support for CI skipped status
+- Fetch code from forks to refs/merge-requests/:id/head when merge request created
+- Remove comments and email addresses when publicly exposing ssh keys (Zeger-Jan van de Weg)
+- Add "Check out branch" button to the MR page.
+- Improve MR merge widget text and UI consistency.
+- Improve text in MR "How To Merge" modal.
+- Cache all events
+- Order commits by date when comparing branches
+- Fix bug causing error when the target branch of a symbolic ref was deleted
+- Include branch/tag name in archive file and directory name
+- Add dropzone upload progress
+- Add a label for merged branches on branches page (Florent Baldino)
+- Detect .mkd and .mkdn files as markdown (Ben Boeckel)
+- Fix: User search feature in admin area does not respect filters
+- Set max-width for README, issue and merge request description for easier read on big screens
+- Update Flowdock integration to support new Flowdock API (Boyan Tabakov)
+- Remove author from files view (Sven Strickroth)
+- Fix infinite loop when SAML was incorrectly configured.
+
+## 7.13.5
+
+- Satellites reverted
+
+## 7.13.4
+
+- Allow users to send abuse reports
+
+## 7.13.3
+
+- Fix bug causing Bitbucket importer to crash when OAuth application had been removed.
+- Allow users to send abuse reports
+- Remove satellites
+- Link username to profile on Group Members page (Tom Webster)
+
+## 7.13.2
+
+- Fix randomly failed spec
+- Create project services on Project creation
+- Add admin_merge_request ability to Developer level and up
+- Fix Error 500 when browsing projects with no HEAD (Stan Hu)
+- Fix labels / assignee / milestone for the merge requests when issues are disabled
+- Show the first tab automatically on MergeRequests#new
+- Add rake task 'gitlab:update_commit_count' (Daniel Gerhardt)
+- Fix Gmail Actions
+
+## 7.13.1
+
+- Fix: Label modifications are not reflected in existing notes and in the issue list
+- Fix: Label not shown in the Issue list, although it's set through web interface
+- Fix: Group/project references are linked incorrectly
+- Improve documentation
+- Fix of migration: Check if session_expire_delay column exists before adding the column
+- Fix: ActionView::Template::Error
+- Fix: "Create Merge Request" isn't always shown in event for newly pushed branch
+- Fix bug causing "Remove source-branch" option not to work for merge requests from the same project.
+- Render Note field hints consistently for "new" and "edit" forms
+
+## 7.13.0
+
+- Remove repository graph log to fix slow cache updates after push event (Stan Hu)
+- Only enable HSTS header for HTTPS and port 443 (Stan Hu)
+- Fix user autocomplete for unauthenticated users accessing public projects (Stan Hu)
+- Fix redirection to home page URL for unauthorized users (Daniel Gerhardt)
+- Add branch switching support for graphs (Daniel Gerhardt)
+- Fix external issue tracker hook/test for HTTPS URLs (Daniel Gerhardt)
+- Remove link leading to a 404 error in Deploy Keys page (Stan Hu)
+- Add support for unlocking users in admin settings (Stan Hu)
+- Add Irker service configuration options (Stan Hu)
+- Fix order of issues imported from GitHub (Hiroyuki Sato)
+- Bump rugments to 1.0.0beta8 to fix C prototype function highlighting (Jonathon Reinhart)
+- Fix Merge Request webhook to properly fire "merge" action when accepted from the web UI
+- Add `two_factor_enabled` field to admin user API (Stan Hu)
+- Fix invalid timestamps in RSS feeds (Rowan Wookey)
+- Fix downloading of patches on public merge requests when user logged out (Stan Hu)
+- Fix Error 500 when relative submodule resolves to a namespace that has a different name from its path (Stan Hu)
+- Extract the longest-matching ref from a commit path when multiple matches occur (Stan Hu)
+- Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu)
+- Support commenting on diffs in side-by-side mode (Stan Hu)
+- Fix JavaScript error when clicking on the comment button on a diff line that has a comment already (Stan Hu)
+- Return 40x error codes if branch could not be deleted in UI (Stan Hu)
+- Remove project visibility icons from dashboard projects list
+- Rename "Design" profile settings page to "Preferences".
+- Allow users to customize their default Dashboard page.
+- Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8
+- Admin can edit and remove user identities
+- Convert CRLF newlines to LF when committing using the web editor.
+- API request /projects/:project_id/merge_requests?state=closed will return only closed merge requests without merged one. If you need ones that were merged - use state=merged.
+- Allow Administrators to filter the user list by those with or without Two-factor Authentication enabled.
+- Show a user's Two-factor Authentication status in the administration area.
+- Explicit error when commit not found in the CI
+- Improve performance for issue and merge request pages
+- Users with guest access level can not set assignee, labels or milestones for issue and merge request
+- Reporter role can manage issue tracker now: edit any issue, set assignee or milestone and manage labels
+- Better performance for pages with events list, issues list and commits list
+- Faster automerge check and merge itself when source and target branches are in same repository
+- Correctly show anonymous authorized applications under Profile > Applications.
+- Query Optimization in MySQL.
+- Allow users to be blocked and unblocked via the API
+- Use native Postgres database cleaning during backup restore
+- Redesign project page. Show README as default instead of activity. Move project activity to separate page
+- Make left menu more hierarchical and less contextual by adding back item at top
+- A fork can’t have a visibility level that is greater than the original project.
+- Faster code search in repository and wiki. Fixes search page timeout for big repositories
+- Allow administrators to disable 2FA for a specific user
+- Add error message for SSH key linebreaks
+- Store commits count in database (will populate with valid values only after first push)
+- Rebuild cache after push to repository in background job
+- Fix transferring of project to another group using the API.
+
+## 7.12.2
+
+- Correctly show anonymous authorized applications under Profile > Applications.
+- Faster automerge check and merge itself when source and target branches are in same repository
+- Audit log for user authentication
+- Allow custom label to be set for authentication providers.
+
+## 7.12.1
+
+- Fix error when deleting a user who has projects (Stan Hu)
+- Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu)
+- Add SAML to list of social_provider (Matt Firtion)
+- Fix merge requests API scope to keep compatibility in 7.12.x patch release (Dmitriy Zaporozhets)
+- Fix closed merge request scope at milestone page (Dmitriy Zaporozhets)
+- Revert merge request states renaming
+- Fix hooks for web based events with external issue references (Daniel Gerhardt)
+- Improve performance for issue and merge request pages
+- Compress database dumps to reduce backup size
+
+## 7.12.0
+
+- Fix Error 500 when one user attempts to access a personal, internal snippet (Stan Hu)
+- Disable changing of target branch in new merge request page when a branch has already been specified (Stan Hu)
+- Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu)
+- Update oauth button logos for Twitter and Google to recommended assets
+- Update browser gem to version 0.8.0 for IE11 support (Stan Hu)
+- Fix timeout when rendering file with thousands of lines.
+- Add "Remember me" checkbox to LDAP signin form.
+- Add session expiration delay configuration through UI application settings
+- Don't notify users mentioned in code blocks or blockquotes.
+- Omit link to generate labels if user does not have access to create them (Stan Hu)
+- Show warning when a comment will add 10 or more people to the discussion.
+- Disable changing of the source branch in merge request update API (Stan Hu)
+- Shorten merge request WIP text.
+- Add option to disallow users from registering any application to use GitLab as an OAuth provider
+- Support editing target branch of merge request (Stan Hu)
+- Refactor permission checks with issues and merge requests project settings (Stan Hu)
+- Fix Markdown preview not working in Edit Milestone page (Stan Hu)
+- Fix Zen Mode not closing with ESC key (Stan Hu)
+- Allow HipChat API version to be blank and default to v2 (Stan Hu)
+- Add file attachment support in Milestone description (Stan Hu)
+- Fix milestone "Browse Issues" button.
+- Set milestone on new issue when creating issue from index with milestone filter active.
+- Make namespace API available to all users (Stan Hu)
+- Add webhook support for note events (Stan Hu)
+- Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu)
+- Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu)
+- Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu)
+- Fix git blame syntax highlighting when different commits break up lines (Stan Hu)
+- Add "Resend confirmation e-mail" link in profile settings (Stan Hu)
+- Allow to configure location of the `.gitlab_shell_secret` file. (Jakub Jirutka)
+- Disabled expansion of top/bottom blobs for new file diffs
+- Update Asciidoctor gem to version 1.5.2. (Jakub Jirutka)
+- Fix resolving of relative links to repository files in AsciiDoc documents. (Jakub Jirutka)
+- Use the user list from the target project in a merge request (Stan Hu)
+- Default extention for wiki pages is now .md instead of .markdown (Jeroen van Baarsen)
+- Add validation to wiki page creation (only [a-zA-Z0-9/_-] are allowed) (Jeroen van Baarsen)
+- Fix new/empty milestones showing 100% completion value (Jonah Bishop)
+- Add a note when an Issue or Merge Request's title changes
+- Consistently refer to MRs as either Merged or Closed.
+- Add Merged tab to MR lists.
+- Prefix EmailsOnPush email subject with `[Git]`.
+- Group project contributions by both name and email.
+- Clarify navigation labels for Project Settings and Group Settings.
+- Move user avatar and logout button to sidebar
+- You can not remove user if he/she is an only owner of group
+- User should be able to leave group. If not - show him proper message
+- User has ability to leave project
+- Add SAML support as an omniauth provider
+- Allow to configure a URL to show after sign out
+- Add an option to automatically sign-in with an Omniauth provider
+- GitLab CI service sends .gitlab-ci.yml in each push call
+- When remove project - move repository and schedule it removal
+- Improve group removing logic
+- Trigger create-hooks on backup restore task
+- Add option to automatically link omniauth and LDAP identities
+- Allow special character in users bio. I.e.: I <3 GitLab
+
+## 7.11.4
+
+- Fix missing bullets when creating lists
+- Set rel="nofollow" on external links
+
+## 7.11.3
+
+- no changes
+- Fix upgrader script (Martins Polakovs)
+
+## 7.11.2
+
+- no changes
+
+## 7.11.1
+
+- no changes
+
+## 7.11.0
+
+- Fall back to Plaintext when Syntaxhighlighting doesn't work. Fixes some buggy lexers (Hannes Rosenögger)
+- Get editing comments to work in Chrome 43 again.
+- Fix broken view when viewing history of a file that includes a path that used to be another file (Stan Hu)
+- Don't show duplicate deploy keys
+- Fix commit time being displayed in the wrong timezone in some cases (Hannes Rosenögger)
+- Make the first branch pushed to an empty repository the default HEAD (Stan Hu)
+- Fix broken view when using a tag to display a tree that contains git submodules (Stan Hu)
+- Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu)
+- Add application setting to restrict user signups to e-mail domains (Stan Hu)
+- Don't allow a merge request to be merged when its title starts with "WIP".
+- Add a page title to every page.
+- Allow primary email to be set to an email that you've already added.
+- Fix clone URL field and X11 Primary selection (Dmitry Medvinsky)
+- Ignore invalid lines in .gitmodules
+- Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu)
+- Redirect to sign in page after signing out.
+- Fix "Hello @username." references not working by no longer allowing usernames to end in period.
+- Fix "Revspec not found" errors when viewing diffs in a forked project with submodules (Stan Hu)
+- Improve project page UI
+- Fix broken file browsing with relative submodule in personal projects (Stan Hu)
+- Add "Reply quoting selected text" shortcut key (`r`)
+- Fix bug causing `@whatever` inside an issue's first code block to be picked up as a user mention.
+- Fix bug causing `@whatever` inside an inline code snippet (backtick-style) to be picked up as a user mention.
+- When use change branches link at MR form - save source branch selection instead of target one
+- Improve handling of large diffs
+- Added GitLab Event header for project hooks
+- Add Two-factor authentication (2FA) for GitLab logins
+- Show Atom feed buttons everywhere where applicable.
+- Add project activity atom feed.
+- Don't crash when an MR from a fork has a cross-reference comment from the target project on one of its commits.
+- Explain how to get a new password reset token in welcome emails
+- Include commit comments in MR from a forked project.
+- Group milestones by title in the dashboard and all other issue views.
+- Query issues, merge requests and milestones with their IID through API (Julien Bianchi)
+- Add default project and snippet visibility settings to the admin web UI.
+- Show incompatible projects in Google Code import status (Stan Hu)
+- Fix bug where commit data would not appear in some subdirectories (Stan Hu)
+- Task lists are now usable in comments, and will show up in Markdown previews.
+- Fix bug where avatar filenames were not actually deleted from the database during removal (Stan Hu)
+- Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu)
+- Protect OmniAuth request phase against CSRF.
+- Don't send notifications to mentioned users that don't have access to the project in question.
+- Add search issues/MR by number
+- Change plots to bar graphs in commit statistics screen
+- Move snippets UI to fluid layout
+- Improve UI for sidebar. Increase separation between navigation and content
+- Improve new project command options (Ben Bodenmiller)
+- Add common method to force UTF-8 and use it to properly handle non-ascii OAuth user properties (Onur Küçük)
+- Prevent sending empty messages to HipChat (Chulki Lee)
+- Improve UI for mobile phones on dashboard and project pages
+- Add room notification and message color option for HipChat
+- Allow to use non-ASCII letters and dashes in project and namespace name. (Jakub Jirutka)
+- Add footnotes support to Markdown (Guillaume Delbergue)
+- Add current_sign_in_at to UserFull REST api.
+- Make Sidekiq MemoryKiller shutdown signal configurable
+- Add "Create Merge Request" buttons to commits and branches pages and push event.
+- Show user roles by comments.
+- Fix automatic blocking of auto-created users from Active Directory.
+- Call merge request webhook for each new commits (Arthur Gautier)
+- Use SIGKILL by default in Sidekiq::MemoryKiller
+- Fix mentioning of private groups.
+- Add style for <kbd> element in markdown
+- Spin spinner icon next to "Checking for CI status..." on MR page.
+- Fix reference links in dashboard activity and ATOM feeds.
+- Ensure that the first added admin performs repository imports
+
+## 7.10.4
+
+- Fix migrations broken in 7.10.2
+- Make tags for GitLab installations running on MySQL case sensitive
+- Get Gitorious importer to work again.
+- Fix adding new group members from admin area
+- Fix DB error when trying to tag a repository (Stan Hu)
+- Fix Error 500 when searching Wiki pages (Stan Hu)
+- Unescape branch names in compare commit (Stan Hu)
+- Order commit comments chronologically in API.
+
+## 7.10.2
+
+- Fix CI links on MR page
+
+## 7.10.0
+
+- Ignore submodules that are defined in .gitmodules but are checked in as directories.
+- Allow projects to be imported from Google Code.
+- Remove access control for uploaded images to fix broken images in emails (Hannes Rosenögger)
+- Allow users to be invited by email to join a group or project.
+- Don't crash when project repository doesn't exist.
+- Add config var to block auto-created LDAP users.
+- Don't use HTML ellipsis in EmailsOnPush subject truncated commit message.
+- Set EmailsOnPush reply-to address to committer email when enabled.
+- Fix broken file browsing with a submodule that contains a relative link (Stan Hu)
+- Fix persistent XSS vulnerability around profile website URLs.
+- Fix project import URL regex to prevent arbitary local repos from being imported.
+- Fix directory traversal vulnerability around uploads routes.
+- Fix directory traversal vulnerability around help pages.
+- Don't leak existence of project via search autocomplete.
+- Don't leak existence of group or project via search.
+- Fix bug where Wiki pages that included a '/' were no longer accessible (Stan Hu)
+- Fix bug where error messages from Dropzone would not be displayed on the issues page (Stan Hu)
+- Add a rake task to check repository integrity with `git fsck`
+- Add ability to configure Reply-To address in gitlab.yml (Stan Hu)
+- Move current user to the top of the list in assignee/author filters (Stan Hu)
+- Fix broken side-by-side diff view on merge request page (Stan Hu)
+- Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu)
+- Allow HTML tags in Markdown input
+- Fix code unfold not working on Compare commits page (Stan Hu)
+- Fix generating SSH key fingerprints with OpenSSH 6.8. (Sašo Stanovnik)
+- Fix "Import projects from" button to show the correct instructions (Stan Hu)
+- Fix dots in Wiki slugs causing errors (Stan Hu)
+- Make maximum attachment size configurable via Application Settings (Stan Hu)
+- Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg)
+- Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu)
+- Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu)
+- Reduce Rack Attack false positives causing 403 errors during HTTP authentication (Stan Hu)
+- enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger)
+- Fix a link in the patch update guide
+- Add a service to support external wikis (Hannes Rosenögger)
+- Omit the "email patches" link and fix plain diff view for merge commits
+- List new commits for newly pushed branch in activity view.
+- Add sidetiq gem dependency to match EE
+- Add changelog, license and contribution guide links to project tab bar.
+- Improve diff UI
+- Fix alignment of navbar toggle button (Cody Mize)
+- Fix checkbox rendering for nested task lists
+- Identical look of selectboxes in UI
+- Upgrade the gitlab_git gem to version 7.1.3
+- Move "Import existing repository by URL" option to button.
+- Improve error message when save profile has error.
+- Passing the name of pushed ref to CI service (requires GitLab CI 7.9+)
+- Add location field to user profile
+- Fix print view for markdown files and wiki pages
+- Fix errors when deleting old backups
+- Improve GitLab performance when working with git repositories
+- Add tag message and last commit to tag hook (Kamil Trzciński)
+- Restrict permissions on backup files
+- Improve oauth accounts UI in profile page
+- Add ability to unlink connected accounts
+- Replace commits calendar with faster contribution calendar that includes issues and merge requests
+- Add inifinite scroll to user page activity
+- Don't include system notes in issue/MR comment count.
+- Don't mark merge request as updated when merge status relative to target branch changes.
+- Link note avatar to user.
+- Make Git-over-SSH errors more descriptive.
+- Fix EmailsOnPush.
+- Refactor issue filtering
+- AJAX selectbox for issue assignee and author filters
+- Fix issue with missing options in issue filtering dropdown if selected one
+- Prevent holding Control-Enter or Command-Enter from posting comment multiple times.
+- Prevent note form from being cleared when submitting failed.
+- Improve file icons rendering on tree (Sullivan Sénéchal)
+- API: Add pagination to project events
+- Get issue links in notification mail to work again.
+- Don't show commit comment button when user is not signed in.
+- Fix admin user projects lists.
+- Don't leak private group existence by redirecting from namespace controller to group controller.
+- Ability to skip some items from backup (database, respositories or uploads)
+- Archive repositories in background worker.
+- Import GitHub, Bitbucket or GitLab.com projects owned by authenticated user into current namespace.
+- Project labels are now available over the API under the "tag_list" field (Cristian Medina)
+- Fixed link paths for HTTP and SSH on the admin project view (Jeremy Maziarz)
+- Fix and improve help rendering (Sullivan Sénéchal)
+- Fix final line in EmailsOnPush email diff being rendered as error.
+- Prevent duplicate Buildkite service creation.
+- Fix git over ssh errors 'fatal: protocol error: bad line length character'
+- Automatically setup GitLab CI project for forks if origin project has GitLab CI enabled
+- Bust group page project list cache when namespace name or path changes.
+- Explicitly set image alt-attribute to prevent graphical glitches if gravatars could not be loaded
+- Allow user to choose a public email to show on public profile
+- Remove truncation from issue titles on milestone page (Jason Blanchard)
+- Fix stuck Merge Request merging events from old installations (Ben Bodenmiller)
+- Fix merge request comments on files with multiple commits
+- Fix Resource Owner Password Authentication Flow
+- Add icons to Add dropdown items.
+- Allow admin to create public deploy keys that are accessible to any project.
+- Warn when gitlab-shell version doesn't match requirement.
+- Skip email confirmation when set by admin or via LDAP.
+- Only allow users to reference groups, projects, issues, MRs, commits they have access to.
+
+## 7.9.4
+
+- Security: Fix project import URL regex to prevent arbitary local repos from being imported
+- Fixed issue where only 25 commits would load in file listings
+- Fix LDAP identities after config update
+
+## 7.9.3
+
+- Contains no changes
+
+## 7.9.2
+
+- Contains no changes
+
+## 7.9.1
+
+- Include missing events and fix save functionality in admin service template settings form (Stan Hu)
+- Fix "Import projects from" button to show the correct instructions (Stan Hu)
+- Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu)
+- Fix for LDAP with commas in DN
+- Fix missing events and in admin Slack service template settings form (Stan Hu)
+- Don't show commit comment button when user is not signed in.
+- Downgrade gemnasium-gitlab-service gem
+
+## 7.9.0
+
+- Add HipChat integration documentation (Stan Hu)
+- Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu)
+- Fix broken email images (Hannes Rosenögger)
+- Automatically config git if user forgot, where possible (Zeger-Jan van de Weg)
+- Fix mass SQL statements on initial push (Hannes Rosenögger)
+- Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu)
+- Add comment notification events to HipChat and Slack services (Stan Hu)
+- Add issue and merge request events to HipChat and Slack services (Stan Hu)
+- Fix merge request URL passed to Webhooks. (Stan Hu)
+- Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu)
+- Fix code preview theme setting for comments, issues, merge requests, and snippets (Stan Hu)
+- Move labels/milestones tabs to sidebar
+- Upgrade Rails gem to version 4.1.9.
+- Improve error messages for file edit failures
+- Improve UI for commits, issues and merge request lists
+- Fix commit comments on first line of diff not rendering in Merge Request Discussion view.
+- Allow admins to override restricted project visibility settings.
+- Move restricted visibility settings from gitlab.yml into the web UI.
+- Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev)
+- Save web edit in new branch
+- Fix ordering of imported but unchanged projects (Marco Wessel)
+- Mobile UI improvements: make aside content expandable
+- Expose avatar_url in projects API
+- Fix checkbox alignment on the application settings page.
+- Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger)
+- Fix mass-unassignment of issues (Robert Speicher)
+- Fix hidden diff comments in merge request discussion view
+- Allow user confirmation to be skipped for new users via API
+- Add a service to send updates to an Irker gateway (Romain Coltel)
+- Add brakeman (security scanner for Ruby on Rails)
+- Slack username and channel options
+- Add grouped milestones from all projects to dashboard.
+- Webhook sends pusher email as well as commiter
+- Add Bitbucket omniauth provider.
+- Add Bitbucket importer.
+- Support referencing issues to a project whose name starts with a digit
+- Condense commits already in target branch when updating merge request source branch.
+- Send notifications and leave system comments when bulk updating issues.
+- Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison)
+- Move groups page from profile to dashboard
+- Starred projects page at dashboard
+- Blocking user does not remove him/her from project/groups but show blocked label
+- Change subject of EmailsOnPush emails to include namespace, project and branch.
+- Change subject of EmailsOnPush emails to include first commit message when multiple were pushed.
+- Remove confusing footer from EmailsOnPush mail body.
+- Add list of changed files to EmailsOnPush emails.
+- Add option to send EmailsOnPush emails from committer email if domain matches.
+- Add option to disable code diffs in EmailOnPush emails.
+- Wrap commit message in EmailsOnPush email.
+- Send EmailsOnPush emails when deleting commits using force push.
+- Fix EmailsOnPush email comparison link to include first commit.
+- Fix highliht of selected lines in file
+- Reject access to group/project avatar if the user doesn't have access.
+- Add database migration to clean group duplicates with same path and name (Make sure you have a backup before update)
+- Add GitLab active users count to rake gitlab:check
+- Starred projects page at dashboard
+- Make email display name configurable
+- Improve json validation in hook data
+- Use Emoji One
+- Updated emoji help documentation to properly reference EmojiOne.
+- Fix missing GitHub organisation repositories on import page.
+- Added blue theme
+- Remove annoying notice messages when create/update merge request
+- Allow smb:// links in Markdown text.
+- Filter merge request by title or description at Merge Requests page
+- Block user if he/she was blocked in Active Directory
+- Fix import pages not working after first load.
+- Use custom LDAP label in LDAP signin form.
+- Execute hooks and services when branch or tag is created or deleted through web interface.
+- Block and unblock user if he/she was blocked/unblocked in Active Directory
+- Raise recommended number of unicorn workers from 2 to 3
+- Use same layout and interactivity for project members as group members.
+- Prevent gitlab-shell character encoding issues by receiving its changes as raw data.
+- Ability to unsubscribe/subscribe to issue or merge request
+- Delete deploy key when last connection to a project is destroyed.
+- Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther)
+- Backup of repositories with tar instead of git bundle (only now are git-annex files included in the backup)
+- Add canceled status for CI
+- Send EmailsOnPush email when branch or tag is created or deleted.
+- Faster merge request processing for large repository
+- Prevent doubling AJAX request with each commit visit via Turbolink
+- Prevent unnecessary doubling of js events on import pages and user calendar
+
+## 7.8.4
+
+- Fix issue_tracker_id substitution in custom issue trackers
+- Fix path and name duplication in namespaces
+
+## 7.8.3
+
+- Bump version of gitlab_git fixing annotated tags without message
+
+## 7.8.2
+
+- Fix service migration issue when upgrading from versions prior to 7.3
+- Fix setting of the default use project limit via admin UI
+- Fix showing of already imported projects for GitLab and Gitorious importers
+- Fix response of push to repository to return "Not found" if user doesn't have access
+- Fix check if user is allowed to view the file attachment
+- Fix import check for case sensetive namespaces
+- Increase timeout for Git-over-HTTP requests to 1 hour since large pulls/pushes can take a long time.
+- Properly handle autosave local storage exceptions.
+- Escape wildcards when searching LDAP by username.
+
+## 7.8.1
+
+- Fix run of custom post receive hooks
+- Fix migration that caused issues when upgrading to version 7.8 from versions prior to 7.3
+- Fix the warning for LDAP users about need to set password
+- Fix avatars which were not shown for non logged in users
+- Fix urls for the issues when relative url was enabled
+
+## 7.8.0
+
+- Fix access control and protection against XSS for note attachments and other uploads.
+- Replace highlight.js with rouge-fork rugments (Stefan Tatschner)
+- Make project search case insensitive (Hannes Rosenögger)
+- Include issue/mr participants in list of recipients for reassign/close/reopen emails
+- Expose description in groups API
+- Better UI for project services page
+- Cleaner UI for web editor
+- Add diff syntax highlighting in email-on-push service notifications (Hannes Rosenögger)
+- Add API endpoint to fetch all changes on a MergeRequest (Jeroen van Baarsen)
+- View note image attachments in new tab when clicked instead of downloading them
+- Improve sorting logic in UI and API. Explicitly define what sorting method is used by default
+- Fix overflow at sidebar when have several items
+- Add notes for label changes in issue and merge requests
+- Show tags in commit view (Hannes Rosenögger)
+- Only count a user's vote once on a merge request or issue (Michael Clarke)
+- Increase font size when browse source files and diffs
+- Service Templates now let you set default values for all services
+- Create new file in empty repository using GitLab UI
+- Ability to clone project using oauth2 token
+- Upgrade Sidekiq gem to version 3.3.0
+- Stop git zombie creation during force push check
+- Show success/error messages for test setting button in services
+- Added Rubocop for code style checks
+- Fix commits pagination
+- Async load a branch information at the commit page
+- Disable blacklist validation for project names
+- Allow configuring protection of the default branch upon first push (Marco Wessel)
+- Add gitlab.com importer
+- Add an ability to login with gitlab.com
+- Add a commit calendar to the user profile (Hannes Rosenögger)
+- Submit comment on command-enter
+- Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`.
+- Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" (Julien Bianchi and Hannes Rosenögger)
+- Fix long broadcast message cut-off on left sidebar (Visay Keo)
+- Add Project Avatars (Steven Thonus and Hannes Rosenögger)
+- Password reset token validity increased from 2 hours to 2 days since it is also send on account creation.
+- Edit group members via API
+- Enable raw image paste from clipboard, currently Chrome only (Marco Cyriacks)
+- Add action property to merge request hook (Julien Bianchi)
+- Remove duplicates from group milestone participants list.
+- Add a new API function that retrieves all issues assigned to a single milestone (Justin Whear and Hannes Rosenögger)
+- API: Access groups with their path (Julien Bianchi)
+- Added link to milestone and keeping resource context on smaller viewports for issues and merge requests (Jason Blanchard)
+- Allow notification email to be set separately from primary email.
+- API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger)
+- Don't have Markdown preview fail for long comments/wiki pages.
+- When test webhook - show error message instead of 500 error page if connection to hook url was reset
+- Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov)
+- Added persistent collapse button for left side nav bar (Jason Blanchard)
+- Prevent losing unsaved comments by automatically restoring them when comment page is loaded again.
+- Don't allow page to be scaled on mobile.
+- Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up.
+- Show assignees in merge request index page (Kelvin Mutuma)
+- Link head panel titles to relevant root page.
+- Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S).
+- Show users button to share their newly created public or internal projects on twitter
+- Add quick help links to the GitLab pricing and feature comparison pages.
+- Fix duplicate authorized applications in user profile and incorrect application client count in admin area.
+- Make sure Markdown previews always use the same styling as the eventual destination.
+- Remove deprecated Group#owner_id from API
+- Show projects user contributed to on user page. Show stars near project on user page.
+- Improve database performance for GitLab
+- Add Asana service (Jeremy Benoist)
+- Improve project webhooks with extra data
+
+## 7.7.2
+
+- Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch
+- Fix issue when LDAP user can't login with existing GitLab account
+
+## 7.7.1
+
+- Improve mention autocomplete performance
+- Show setup instructions for GitHub import if disabled
+- Allow use http for OAuth applications
+
+## 7.7.0
+
+- Import from GitHub.com feature
+- Add Jetbrains Teamcity CI service (Jason Lippert)
+- Mention notification level
+- Markdown preview in wiki (Yuriy Glukhov)
+- Raise group avatar filesize limit to 200kb
+- OAuth applications feature
+- Show user SSH keys in admin area
+- Developer can push to protected branches option
+- Set project path instead of project name in create form
+- Block Git HTTP access after 10 failed authentication attempts
+- Updates to the messages returned by API (sponsored by O'Reilly Media)
+- New UI layout with side navigation
+- Add alert message in case of outdated browser (IE < 10)
+- Added API support for sorting projects
+- Update gitlab_git to version 7.0.0.rc14
+- Add API project search filter option for authorized projects
+- Fix File blame not respecting branch selection
+- Change some of application settings on fly in admin area UI
+- Redesign signin/signup pages
+- Close standard input in Gitlab::Popen.popen
+- Trigger GitLab CI when push tags
+- When accept merge request - do merge using sidaekiq job
+- Enable web signups by default
+- Fixes for diff comments: drag-n-drop images, selecting images
+- Fixes for edit comments: drag-n-drop images, preview mode, selecting images, save & update
+- Remove password strength indicator
+
+## 7.6.0
+
+- Fork repository to groups
+- New rugged version
+- Add CRON=1 backup setting for quiet backups
+- Fix failing wiki restore
+- Add optional Sidekiq MemoryKiller middleware (enabled via SIDEKIQ_MAX_RSS env variable)
+- Monokai highlighting style now more faithful to original design (Mark Riedesel)
+- Create project with repository in synchrony
+- Added ability to create empty repo or import existing one if project does not have repository
+- Reactivate highlight.js language autodetection
+- Mobile UI improvements
+- Change maximum avatar file size from 100KB to 200KB
+- Strict validation for snippet file names
+- Enable Markdown preview for issues, merge requests, milestones, and notes (Vinnie Okada)
+- In the docker directory is a container template based on the Omnibus packages.
+- Update Sidekiq to version 2.17.8
+- Add author filter to project issues and merge requests pages
+- Atom feed for user activity
+- Support multiple omniauth providers for the same user
+- Rendering cross reference in issue title and tooltip for merge request
+- Show username in comments
+- Possibility to create Milestones or Labels when Issues are disabled
+- Fix bug with showing gpg signature in tag
+
+## 7.5.3
+
+- Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2)
+
+## 7.5.2
+
+- Don't log Sidekiq arguments by default
+- Fix restore of wiki repositories from backups
+
+## 7.5.1
+
+- Add missing timestamps to 'members' table
+
+## 7.5.0
+
+- API: Add support for Hipchat (Kevin Houdebert)
+- Add time zone configuration in gitlab.yml (Sullivan Senechal)
+- Fix LDAP authentication for Git HTTP access
+- Run 'GC.start' after every EmailsOnPushWorker job
+- Fix LDAP config lookup for provider 'ldap'
+- Drop all sequences during Postgres database restore
+- Project title links to project homepage (Ben Bodenmiller)
+- Add Atlassian Bamboo CI service (Drew Blessing)
+- Mentioned @user will receive email even if he is not participating in issue or commit
+- Session API: Use case-insensitive authentication like in UI (Andrey Krivko)
+- Tie up loose ends with annotated tags: API & UI (Sean Edge)
+- Return valid json for deleting branch via API (sponsored by O'Reilly Media)
+- Expose username in project events API (sponsored by O'Reilly Media)
+- Adds comments to commits in the API
+- Performance improvements
+- Fix post-receive issue for projects with deleted forks
+- New gitlab-shell version with custom hooks support
+- Improve code
+- GitLab CI 5.2+ support (does not support older versions)
+- Fixed bug when you can not push commits starting with 000000 to protected branches
+- Added a password strength indicator
+- Change project name and path in one form
+- Display renamed files in diff views (Vinnie Okada)
+- Fix raw view for public snippets
+- Use secret token with GitLab internal API.
+- Add missing timestamps to 'members' table
+
+## 7.4.5
+
+- Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2)
+
+## 7.4.4
+
+- No changes
+
+## 7.4.3
+
+- Fix raw snippets view
+- Fix security issue for member api
+- Fix buildbox integration
+
+## 7.4.2
+
+- Fix internal snippet exposing for unauthenticated users
+
+## 7.4.1
+
+- Fix LDAP authentication for Git HTTP access
+- Fix LDAP config lookup for provider 'ldap'
+- Fix public snippets
+- Fix 500 error on projects with nested submodules
+
+## 7.4.0
+
+- Refactored membership logic
+- Improve error reporting on users API (Julien Bianchi)
+- Refactor test coverage tools usage. Use SIMPLECOV=true to generate it locally
+- Default branch is protected by default
+- Increase unicorn timeout to 60 seconds
+- Sort search autocomplete projects by stars count so most popular go first
+- Add README to tab on project show page
+- Do not delete tmp/repositories itself during clean-up, only its contents
+- Support for backup uploads to remote storage
+- Prevent notes polling when there are not notes
+- Internal ForkService: Prepare support for fork to a given namespace
+- API: Add support for forking a project via the API (Bernhard Kaindl)
+- API: filter project issues by milestone (Julien Bianchi)
+- Fail harder in the backup script
+- Changes to Slack service structure, only webhook url needed
+- Zen mode for wiki and milestones (Robert Schilling)
+- Move Emoji parsing to html-pipeline-gitlab (Robert Schilling)
+- Font Awesome 4.2 integration (Sullivan Senechal)
+- Add Pushover service integration (Sullivan Senechal)
+- Add select field type for services options (Sullivan Senechal)
+- Add cross-project references to the Markdown parser (Vinnie Okada)
+- Add task lists to issue and merge request descriptions (Vinnie Okada)
+- Snippets can be public, internal or private
+- Improve danger zone: ask project path to confirm data-loss action
+- Raise exception on forgery
+- Show build coverage in Merge Requests (requires GitLab CI v5.1)
+- New milestone and label links on issue edit form
+- Improved repository graphs
+- Improve event note display in dashboard and project activity views (Vinnie Okada)
+- Add users sorting to admin area
+- UI improvements
+- Fix ambiguous sha problem with mentioned commit
+- Fixed bug with apostrophe when at mentioning users
+- Add active directory ldap option
+- Developers can push to wiki repo. Protected branches does not affect wiki repo any more
+- Faster rev list
+- Fix branch removal
+
+## 7.3.2
+
+- Fix creating new file via web editor
+- Use gitlab-shell v2.0.1
+
+## 7.3.1
+
+- Fix ref parsing in Gitlab::GitAccess
+- Fix error 500 when viewing diff on a file with changed permissions
+- Fix adding comments to MR when source branch is master
+- Fix error 500 when searching description contains relative link
+
+## 7.3.0
+
+- Always set the 'origin' remote in satellite actions
+- Write authorized_keys in tmp/ during tests
+- Use sockets to connect to Redis
+- Add dormant New Relic gem (can be enabled via environment variables)
+- Expire Rack sessions after 1 week
+- Cleaner signin/signup pages
+- Improved comments UI
+- Better search with filtering, pagination etc
+- Added a checkbox to toggle line wrapping in diff (Yuriy Glukhov)
+- Prevent project stars duplication when fork project
+- Use the default Unicorn socket backlog value of 1024
+- Support Unix domain sockets for Redis
+- Store session Redis keys in 'session:gitlab:' namespace
+- Deprecate LDAP account takeover based on partial LDAP email / GitLab username match
+- Use /bin/sh instead of Bash in bin/web, bin/background_jobs (Pavel Novitskiy)
+- Keyboard shortcuts for productivity (Robert Schilling)
+- API: filter issues by state (Julien Bianchi)
+- API: filter issues by labels (Julien Bianchi)
+- Add system hook for ssh key changes
+- Add blob permalink link (Ciro Santilli)
+- Create annotated tags through UI and API (Sean Edge)
+- Snippets search (Charles Bushong)
+- Comment new push to existing MR
+- Add 'ci' to the blacklist of forbidden names
+- Improve text filtering on issues page
+- Comment & Close button
+- Process git push --all much faster
+- Don't allow edit of system notes
+- Project wiki search (Ralf Seidler)
+- Enabled Shibboleth authentication support (Matus Banas)
+- Zen mode (fullscreen) for issues/MR/notes (Robert Schilling)
+- Add ability to configure webhook timeout via gitlab.yml (Wes Gurney)
+- Sort project merge requests in asc or desc order for updated_at or created_at field (sponsored by O'Reilly Media)
+- Add Redis socket support to 'rake gitlab:shell:install'
+
+## 7.2.1
+
+- Delete orphaned labels during label migration (James Brooks)
+- Security: prevent XSS with stricter MIME types for raw repo files
+
+## 7.2.0
+
+- Explore page
+- Add project stars (Ciro Santilli)
+- Log Sidekiq arguments
+- Better labels: colors, ability to rename and remove
+- Improve the way merge request collects diffs
+- Improve compare page for large diffs
+- Expose the full commit message via API
+- Fix 500 error on repository rename
+- Fix bug when MR download patch return invalid diff
+- Test gitlab-shell integration
+- Repository import timeout increased from 2 to 4 minutes allowing larger repos to be imported
+- API for labels (Robert Schilling)
+- API: ability to set an import url when creating project for specific user
+
+## 7.1.1
+
+- Fix cpu usage issue in Firefox
+- Fix redirect loop when changing password by new user
+- Fix 500 error on new merge request page
+
+## 7.1.0
+
+- Remove observers
+- Improve MR discussions
+- Filter by description on Issues#index page
+- Fix bug with namespace select when create new project page
+- Show README link after description for non-master members
+- Add @all mention for comments
+- Dont show reply button if user is not signed in
+- Expose more information for issues with webhook
+- Add a mention of the merge request into the default merge request commit message
+- Improve code highlight, introduce support for more languages like Go, Clojure, Erlang etc
+- Fix concurrency issue in repository download
+- Dont allow repository name start with ?
+- Improve email threading (Pierre de La Morinerie)
+- Cleaner help page
+- Group milestones
+- Improved email notifications
+- Contributors API (sponsored by Mobbr)
+- Fix LDAP TLS authentication (Boris HUISGEN)
+- Show VERSION information on project sidebar
+- Improve branch removal logic when accept MR
+- Fix bug where comment form is spawned inside the Reply button
+- Remove Dir.chdir from Satellite#lock for thread-safety
+- Increased default git max_size value from 5MB to 20MB in gitlab.yml. Please update your configs!
+- Show error message in case of timeout in satellite when create MR
+- Show first 100 files for huge diff instead of hiding all
+- Change default admin email from admin@local.host to admin@example.com
+
+## 7.0.0
+
+- The CPU no longer overheats when you hold down the spacebar
+- Improve edit file UI
+- Add ability to upload group avatar when create
+- Protected branch cannot be removed
+- Developers can remove normal branches with UI
+- Remove branch via API (sponsored by O'Reilly Media)
+- Move protected branches page to Project settings area
+- Redirect to Files view when create new branch via UI
+- Drag and drop upload of image in every markdown-area (Earle Randolph Bunao and Neil Francis Calabroso)
+- Refactor the markdown relative links processing
+- Make it easier to implement other CI services for GitLab
+- Group masters can create projects in group
+- Deprecate ruby 1.9.3 support
+- Only masters can rewrite/remove git tags
+- Add X-Frame-Options SAMEORIGIN to Nginx config so Sidekiq admin is visible
+- UI improvements
+- Case-insensetive search for issues
+- Update to rails 4.1
+- Improve performance of application for projects and groups with a lot of members
+- Formally support Ruby 2.1
+- Include Nginx gitlab-ssl config
+- Add manual language detection for highlight.js
+- Added example.com/:username routing
+- Show notice if your profile is public
+- UI improvements for mobile devices
+- Improve diff rendering performance
+- Drag-n-drop for issues and merge requests between states at milestone page
+- Fix '0 commits' message for huge repositories on project home page
+- Prevent 500 error page when visit commit page from large repo
+- Add notice about huge push over http to unicorn config
+- File action in satellites uses default 30 seconds timeout instead of old 10 seconds one
+- Overall performance improvements
+- Skip init script check on omnibus-gitlab
+- Be more selective when killing stray Sidekiqs
+- Check LDAP user filter during sign-in
+- Remove wall feature (no data loss - you can take it from database)
+- Dont expose user emails via API unless you are admin
+- Detect issues closed by Merge Request description
+- Better email subject lines from email on push service (Alex Elman)
+- Enable identicon for gravatar be default
+
+## 6.9.2
+
+- Revert the commit that broke the LDAP user filter
+
+## 6.9.1
+
+- Fix scroll to highlighted line
+- Fix the pagination on load for commits page
+
+## 6.9.0
+
+- Store Rails cache data in the Redis `cache:gitlab` namespace
+- Adjust MySQL limits for existing installations
+- Add db index on project_id+iid column. This prevents duplicate on iid (During migration duplicates will be removed)
+- Markdown preview or diff during editing via web editor (Evgeniy Sokovikov)
+- Give the Rails cache its own Redis namespace
+- Add ability to set different ssh host, if different from http/https
+- Fix syntax highlighting for code comments blocks
+- Improve comments loading logic
+- Stop refreshing comments when the tab is hidden
+- Improve issue and merge request mobile UI (Drew Blessing)
+- Document how to convert a backup to PostgreSQL
+- Fix locale bug in backup manager
+- Fix can not automerge when MR description is too long
+- Fix wiki backup skip bug
+- Two Step MR creation process
+- Remove unwanted files from satellite working directory with git clean -fdx
+- Accept merge request via API (sponsored by O'Reilly Media)
+- Add more access checks during API calls
+- Block SSH access for 'disabled' Active Directory users
+- Labels for merge requests (Drew Blessing)
+- Threaded emails by setting a Message-ID (Philip Blatter)
+
+## 6.8.0
+
+- Ability to at mention users that are participating in issue and merge req. discussion
+- Enabled GZip Compression for assets in example Nginx, make sure that Nginx is compiled with --with-http_gzip_static_module flag (this is default in Ubuntu)
+- Make user search case-insensitive (Christopher Arnold)
+- Remove omniauth-ldap nickname bug workaround
+- Drop all tables before restoring a Postgres backup
+- Make the repository downloads path configurable
+- Create branches via API (sponsored by O'Reilly Media)
+- Changed permission of gitlab-satellites directory not to be world accessible
+- Protected branch does not allow force push
+- Fix popen bug in `rake gitlab:satellites:create`
+- Disable connection reaping for MySQL
+- Allow oauth signup without email for twitter and github
+- Fix faulty namespace names that caused 500 on user creation
+- Option to disable standard login
+- Clean old created archives from repository downloads directory
+- Fix download link for huge MR diffs
+- Expose event and mergerequest timestamps in API
+- Fix emails on push service when only one commit is pushed
+
+## 6.7.3
+
+- Fix the merge notification email not being sent (Pierre de La Morinerie)
+- Drop all tables before restoring a Postgres backup
+- Remove yanked modernizr gem
+
+## 6.7.2
+
+- Fix upgrader script
+
+## 6.7.1
+
+- Fix GitLab CI integration
+
+## 6.7.0
+
+- Increased the example Nginx client_max_body_size from 5MB to 20MB, consider updating it manually on existing installations
+- Add support for Gemnasium as a Project Service (Olivier Gonzalez)
+- Add edit file button to MergeRequest diff
+- Public groups (Jason Hollingsworth)
+- Cleaner headers in Notification Emails (Pierre de La Morinerie)
+- Blob and tree gfm links to anchors work
+- Piwik Integration (Sebastian Winkler)
+- Show contribution guide link for new issue form (Jeroen van Baarsen)
+- Fix CI status for merge requests from fork
+- Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard)
+- New page load indicator that includes a spinner that scrolls with the page
+- Converted all the help sections into markdown
+- LDAP user filters
+- Streamline the content of notification emails (Pierre de La Morinerie)
+- Fixes a bug with group member administration (Matt DeTullio)
+- Sort tag names using VersionSorter (Robert Speicher)
+- Add GFM autocompletion for MergeRequests (Robert Speicher)
+- Add webhook when a new tag is pushed (Jeroen van Baarsen)
+- Add button for toggling inline comments in diff view
+- Add retry feature for repository import
+- Reuse the GitLab LDAP connection within each request
+- Changed markdown new line behaviour to conform to markdown standards
+- Fix global search
+- Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5)
+- Create and Update MR calls now support the description parameter (Greg Messner)
+- Markdown relative links in the wiki link to wiki pages, markdown relative links in repositories link to files in the repository
+- Added Slack service integration (Federico Ravasio)
+- Better API responses for access_levels (sponsored by O'Reilly Media)
+- Requires at least 2 unicorn workers
+- Requires gitlab-shell v1.9+
+- Replaced gemoji(due to closed licencing problem) with Phantom Open Emoji library(combined SIL Open Font License, MIT License and the CC 3.0 License)
+- Fix `/:username.keys` response content type (Dmitry Medvinsky)
+
+## 6.6.5
+
+- Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard)
+- Hide mr close button for comment form if merge request was closed or inline comment
+- Adds ability to reopen closed merge request
+
+## 6.6.4
+
+- Add missing html escape for highlighted code blocks in comments, issues
+
+## 6.6.3
+
+- Fix 500 error when edit yourself from admin area
+- Hide private groups for public profiles
+
+## 6.6.2
+
+- Fix 500 error on branch/tag create or remove via UI
+
+## 6.6.1
+
+- Fix 500 error on files tab if submodules presents
+
+## 6.6.0
+
+- Retrieving user ssh keys publically(github style): http://__HOST__/__USERNAME__.keys
+- Permissions: Developer now can manage issue tracker (modify any issue)
+- Improve Code Compare page performance
+- Group avatar
+- Pygments.rb replaced with highlight.js
+- Improve Merge request diff store logic
+- Improve render performnace for MR show page
+- Fixed Assembla hardcoded project name
+- Jira integration documentation
+- Refactored app/services
+- Remove snippet expiration
+- Mobile UI improvements (Drew Blessing)
+- Fix block/remove UI for admin::users#show page
+- Show users' group membership on users' activity page (Robert Djurasaj)
+- User pages are visible without login if user is authorized to a public project
+- Markdown rendered headers have id derived from their name and link to their id
+- Improve application to work faster with large groups (100+ members)
+- Multiple emails per user
+- Show last commit for file when view file source
+- Restyle Issue#show page and MR#show page
+- Ability to filter by multiple labels for Issues page
+- Rails version to 4.0.3
+- Fixed attachment identifier displaying underneath note text (Jason Blanchard)
+
+## 6.5.1
+
+- Fix branch selectbox when create merge request from fork
+
+## 6.5.0
+
+- Dropdown menus on issue#show page for assignee and milestone (Jason Blanchard)
+- Add color custimization and previewing to broadcast messages
+- Fixed notes anchors
+- Load new comments in issues dynamically
+- Added sort options to Public page
+- New filters (assigned/authored/all) for Dashboard#issues/merge_requests (sponsored by Say Media)
+- Add project visibility icons to dashboard
+- Enable secure cookies if https used
+- Protect users/confirmation with rack_attack
+- Default HTTP headers to protect against MIME-sniffing, force https if enabled
+- Bootstrap 3 with responsive UI
+- New repository download formats: tar.bz2, zip, tar (Jason Hollingsworth)
+- Restyled accept widgets for MR
+- SCSS refactored
+- Use jquery timeago plugin
+- Fix 500 error for rdoc files
+- Ability to customize merge commit message (sponsored by Say Media)
+- Search autocomplete via ajax
+- Add website url to user profile
+- Files API supports base64 encoded content (sponsored by O'Reilly Media)
+- Added support for Go's repository retrieval (Bruno Albuquerque)
+
+## 6.4.3
+
+- Don't use unicorn worker killer if PhusionPassenger is defined
+
+## 6.4.2
+
+- Fixed wrong behaviour of script/upgrade.rb
+
+## 6.4.1
+
+- Fixed bug with repository rename
+- Fixed bug with project transfer
+
+## 6.4.0
+
+- Added sorting to project issues page (Jason Blanchard)
+- Assembla integration (Carlos Paramio)
+- Fixed another 500 error with submodules
+- UI: More compact issues page
+- Minimal password length increased to 8 symbols
+- Side-by-side diff view (Steven Thonus)
+- Internal projects (Jason Hollingsworth)
+- Allow removal of avatar (Drew Blessing)
+- Project webhooks now support issues and merge request events
+- Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth)
+- Expire event cache on avatar creation/removal (Drew Blessing)
+- Archiving old projects (Steven Thonus)
+- Rails 4
+- Add time ago tooltips to show actual date/time
+- UI: Fixed UI for admin system hooks
+- Ruby script for easier GitLab upgrade
+- Do not remove Merge requests if fork project was removed
+- Improve sign-in/signup UX
+- Add resend confirmation link to sign-in page
+- Set noreply@HOSTNAME for reply_to field in all emails
+- Show GitLab API version on Admin#dashboard
+- API Cross-origin resource sharing
+- Show READMe link at project home page
+- Show repo size for projects in Admin area
+
+## 6.3.0
+
+- API for adding gitlab-ci service
+- Init script now waits for pids to appear after (re)starting before reporting status (Rovanion Luckey)
+- Restyle project home page
+- Grammar fixes
+- Show branches list (which branches contains commit) on commit page (Andrew Kumanyaev)
+- Security improvements
+- Added support for GitLab CI 4.0
+- Fixed issue with 500 error when group did not exist
+- Ability to leave project
+- You can create file in repo using UI
+- You can remove file from repo using UI
+- API: dropped default_branch attribute from project during creation
+- Project default_branch is not stored in db any more. It takes from repo now.
+- Admin broadcast messages
+- UI improvements
+- Dont show last push widget if user removed this branch
+- Fix 500 error for repos with newline in file name
+- Extended html titles
+- API: create/update/delete repo files
+- Admin can transfer project to any namespace
+- API: projects/all for admin users
+- Fix recent branches order
+
+## 6.2.4
+
+- Security: Cast API private_token to string (CVE-2013-4580)
+- Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583)
+- Fix for Git SSH access for LDAP users
+
+## 6.2.3
+
+- Security: More protection against CVE-2013-4489
+- Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546)
+- Fix sidekiq rake tasks
+
+## 6.2.2
+
+- Security: Update gitlab_git (CVE-2013-4489)
+
+## 6.2.1
+
+- Security: Fix issue with generated passwords for new users
+
+## 6.2.0
+
+- Public project pages are now visible to everyone (files, issues, wik, etc.)
+ THIS MEANS YOUR ISSUES AND WIKI FOR PUBLIC PROJECTS ARE PUBLICLY VISIBLE AFTER THE UPGRADE
+- Add group access to permissions page
+- Require current password to change one
+- Group owner or admin can remove other group owners
+- Remove group transfer since we have multiple owners
+- Respect authorization in Repository API
+- Improve UI for Project#files page
+- Add more security specs
+- Added search for projects by name to api (Izaak Alpert)
+- Make default user theme configurable (Izaak Alpert)
+- Update logic for validates_merge_request for tree of MR (Andrew Kumanyaev)
+- Rake tasks for webhooks management (Jonhnny Weslley)
+- Extended User API to expose admin and can_create_group for user creation/updating (Boyan Tabakov)
+- API: Remove group
+- API: Remove project
+- Avatar upload on profile page with a maximum of 100KB (Steven Thonus)
+- Store the sessions in Redis instead of the cookie store
+- Fixed relative links in markdown
+- User must confirm their email if signup enabled
+- User must confirm changed email
+
+## 6.1.0
+
+- Project specific IDs for issues, mr, milestones
+ Above items will get a new id and for example all bookmarked issue urls will change.
+ Old issue urls are redirected to the new one if the issue id is too high for an internal id.
+- Description field added to Merge Request
+- API: Sudo api calls (Izaak Alpert)
+- API: Group membership api (Izaak Alpert)
+- Improved commit diff
+- Improved large commit handling (Boyan Tabakov)
+- Rewrite: Init script now less prone to errors and keeps better track of the service (Rovanion Luckey)
+- Link issues, merge requests, and commits when they reference each other with GFM (Ash Wilson)
+- Close issues automatically when pushing commits with a special message
+- Improve user removal from admin area
+- Invalidate events cache when project was moved
+- Remove deprecated classes and rake tasks
+- Add event filter for group and project show pages
+- Add links to create branch/tag from project home page
+- Add public-project? checkbox to new-project view
+- Improved compare page. Added link to proceed into Merge Request
+- Send an email to a user when they are added to group
+- New landing page when you have 0 projects
+
+## 6.0.0
+
+- Feature: Replace teams with group membership
+ We introduce group membership in 6.0 as a replacement for teams.
+ The old combination of groups and teams was confusing for a lot of people.
+ And when the members of a team where changed this wasn't reflected in the project permissions.
+ In GitLab 6.0 you will be able to add members to a group with a permission level for each member.
+ These group members will have access to the projects in that group.
+ Any changes to group members will immediately be reflected in the project permissions.
+ You can even have multiple owners for a group, greatly simplifying administration.
+- Feature: Ability to have multiple owners for group
+- Feature: Merge Requests between fork and project (Izaak Alpert)
+- Feature: Generate fingerprint for ssh keys
+- Feature: Ability to create and remove branches with UI
+- Feature: Ability to create and remove git tags with UI
+- Feature: Groups page in profile. You can leave group there
+- API: Allow login with LDAP credentials
+- Redesign: project settings navigation
+- Redesign: snippets area
+- Redesign: ssh keys page
+- Redesign: buttons, blocks and other ui elements
+- Add comment title to rss feed
+- You can use arrows to navigate at tree view
+- Add project filter on dashboard
+- Cache project graph
+- Drop support of root namespaces
+- Default theme is classic now
+- Cache result of methods like authorize_projects, project.team.members etc
+- Remove $.ready events
+- Fix onclick events being double binded
+- Add notification level to group membership
+- Move all project controllers/views under Projects:: module
+- Move all profile controllers/views under Profiles:: module
+- Apply user project limit only for personal projects
+- Unicorn is default web server again
+- Store satellites lock files inside satellites dir
+- Disabled threadsafety mode in rails
+- Fixed bug with loosing MR comments
+- Improved MR comments logic
+- Render readme file for projects in public area
+
+## 5.4.2
+
+- Security: Cast API private_token to string (CVE-2013-4580)
+- Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583)
+
+## 5.4.1
+
+- Security: Fixes for CVE-2013-4489
+- Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546)
+
+## 5.4.0
+
+- Ability to edit own comments
+- Documentation improvements
+- Improve dashboard projects page
+- Fixed nav for empty repos
+- GitLab Markdown help page
+- Misspelling fixes
+- Added support of unicorn and fog gems
+- Added client list to API doc
+- Fix PostgreSQL database restoration problem
+- Increase snippet content column size
+- allow project import via git:// url
+- Show participants on issues, including mentions
+- Notify mentioned users with email
+
+## 5.3.0
+
+- Refactored services
+- Campfire service added
+- HipChat service added
+- Fixed bug with LDAP + git over http
+- Fixed bug with google analytics code being ignored
+- Improve sign-in page if ldap enabled
+- Respect newlines in wall messages
+- Generate the Rails secret token on first run
+- Rename repo feature
+- Init.d: remove gitlab.socket on service start
+- Api: added teams api
+- Api: Prevent blob content being escaped
+- Api: Smart deploy key add behaviour
+- Api: projects/owned.json return user owned project
+- Fix bug with team assignation on project from #4109
+- Advanced snippets: public/private, project/personal (Andrew Kulakov)
+- Repository Graphs (Karlo Nicholas T. Soriano)
+- Fix dashboard lost if comment on commit
+- Update gitlab-grack. Fixes issue with --depth option
+- Fix project events duplicate on project page
+- Fix postgres error when displaying network graph.
+- Fix dashboard event filter when navigate via turbolinks
+- init.d: Ensure socket is removed before starting service
+- Admin area: Style teams:index, group:show pages
+- Own page for failed forking
+- Scrum view for milestone
+
+## 5.2.0
+
+- Turbolinks
+- Git over http with ldap credentials
+- Diff with better colors and some spacing on the corners
+- Default values for project features
+- Fixed huge_commit view
+- Restyle project clone panel
+- Move Gitlab::Git code to gitlab_git gem
+- Move update docs in repo
+- Requires gitlab-shell v1.4.0
+- Fixed submodules listing under file tab
+- Fork feature (Angus MacArthur)
+- git version check in gitlab:check
+- Shared deploy keys feature
+- Ability to generate default labels set for issues
+- Improve gfm autocomplete (Harold Luo)
+- Added support for Google Analytics
+- Code search feature (Javier Castro)
+
+## 5.1.0
+
+- You can login with email or username now
+- Corrected project transfer rollback when repository cannot be moved
+- Move both repo and wiki when project transfer requested
+- Admin area: project editing was removed from admin namespace
+- Access: admin user has now access to any project.
+- Notification settings
+- Gitlab::Git set of objects to abstract from grit library
+- Replace Unicorn web server with Puma
+- Backup/Restore refactored. Backup dump project wiki too now
+- Restyled Issues list. Show milestone version in issue row
+- Restyled Merge Request list
+- Backup now dump/restore uploads
+- Improved performance of dashboard (Andrew Kumanyaev)
+- File history now tracks renames (Akzhan Abdulin)
+- Drop wiki migration tools
+- Drop sqlite migration tools
+- project tagging
+- Paginate users in API
+- Restyled network graph (Hiroyuki Sato)
+
+## 5.0.1
+
+- Fixed issue with gitlab-grit being overridden by grit
+
+## 5.0.0
+
+- Replaced gitolite with gitlab-shell
+- Removed gitolite-related libraries
+- State machine added
+- Setup gitlab as git user
+- Internal API
+- Show team tab for empty projects
+- Import repository feature
+- Updated rails
+- Use lambda for scopes
+- Redesign admin area -> users
+- Redesign admin area -> user
+- Secure link to file attachments
+- Add validations for Group and Team names
+- Restyle team page for project
+- Update capybara, rspec-rails, poltergeist to recent versions
+- Wiki on git using Gollum
+- Added Solarized Dark theme for code review
+- Don't show user emails in autocomplete lists, profile pages
+- Added settings tab for group, team, project
+- Replace user popup with icons in header
+- Handle project moving with gitlab-shell
+- Added select2-rails for selectboxes with ajax data load
+- Fixed search field on projects page
+- Added teams to search autocomplete
+- Move groups and teams on dashboard sidebar to sub-tabs
+- API: improved return codes and docs. (Felix Gilcher, Sebastian Ziebell)
+- Redesign wall to be more like chat
+- Snippets, Wall features are disabled by default for new projects
+
+## 4.2.0
+
+- Teams
+- User show page. Via /u/username
+- Show help contents on pages for better navigation
+- Async gitolite calls
+- added satellites logs
+- can_create_group, can_create_team booleans for User
+- Process webhooks async
+- GFM: Fix images escaped inside links
+- Network graph improved
+- Switchable branches for network graph
+- API: Groups
+- Fixed project download
+
+## 4.1.0
+
+- Optional Sign-Up
+- Discussions
+- Satellites outside of tmp
+- Line numbers for blame
+- Project public mode
+- Public area with unauthorized access
+- Load dashboard events with ajax
+- remember dashboard filter in cookies
+- replace resque with sidekiq
+- fix routing issues
+- cleanup rake tasks
+- fix backup/restore
+- scss cleanup
+- show preview for note images
+- improved network-graph
+- get rid of app/roles/
+- added new classes Team, Repository
+- Reduce amount of gitolite calls
+- Ability to add user in all group projects
+- remove deprecated configs
+- replaced Korolev font with open font
+- restyled admin/dashboard page
+- restyled admin/projects page
+
+## 4.0.0
+
+- Remove project code and path from API. Use id instead
+- Return valid cloneable url to repo for webhook
+- Fixed backup issue
+- Reorganized settings
+- Fixed commits compare
+- Refactored scss
+- Improve status checks
+- Validates presence of User#name
+- Fixed postgres support
+- Removed sqlite support
+- Modified post-receive hook
+- Milestones can be closed now
+- Show comment events on dashboard
+- Quick add team members via group#people page
+- [API] expose created date for hooks and SSH keys
+- [API] list, create issue notes
+- [API] list, create snippet notes
+- [API] list, create wall notes
+- Remove project code - use path instead
+- added username field to user
+- rake task to fill usernames based on emails create namespaces for users
+- STI Group < Namespace
+- Project has namespace_id
+- Projects with namespaces also namespaced in gitolite and stored in subdir
+- Moving project to group will move it under group namespace
+- Ability to move project from namespaces to another
+- Fixes commit patches getting escaped (see #2036)
+- Support diff and patch generation for commits and merge request
+- MergeReqest doesn't generate a temporary file for the patch any more
+- Update the UI to allow downloading Patch or Diff
+
+## 3.1.0
+
+- Updated gems
+- Services: Gitlab CI integration
+- Events filter on dashboard
+- Own namespace for redis/resque
+- Optimized commit diff views
+- add alphabetical order for projects admin page
+- Improved web editor
+- Commit stats page
+- Documentation split and cleanup
+- Link to commit authors everywhere
+- Restyled milestones list
+- added Milestone to Merge Request
+- Restyled Top panel
+- Refactored Satellite Code
+- Added file line links
+- moved from capybara-webkit to poltergeist + phantomjs
+
+## 3.0.3
+
+- Fixed bug with issues list in Chrome
+- New Feature: Import team from another project
+
+## 3.0.2
+
+- Fixed gitlab:app:setup
+- Fixed application error on empty project in admin area
+- Restyled last push widget
+
+## 3.0.1
+
+- Fixed git over http
+
+## 3.0.0
+
+- Projects groups
+- Web Editor
+- Fixed bug with gitolite keys
+- UI improved
+- Increased performance of application
+- Show user avatar in last commit when browsing Files
+- Refactored Gitlab::Merge
+- Use Font Awesome for icons
+- Separate observing of Note and MergeRequests
+- Milestone "All Issues" filter
+- Fix issue close and reopen button text and styles
+- Fix forward/back while browsing Tree hierarchy
+- Show number of notes for commits and merge requests
+- Added support pg from box and update installation doc
+- Reject ssh keys that break gitolite
+- [API] list one project hook
+- [API] edit project hook
+- [API] list project snippets
+- [API] allow to authorize using private token in HTTP header
+- [API] add user creation
+
+## 2.9.1
+
+- Fixed resque custom config init
+
+## 2.9.0
+
+- fixed inline notes bugs
+- refactored rspecs
+- refactored gitolite backend
+- added factory_girl
+- restyled projects list on dashboard
+- ssh keys validation to prevent gitolite crash
+- send notifications if changed permission in project
+- scss refactoring. gitlab_bootstrap/ dir
+- fix git push http body bigger than 112k problem
+- list of labels page under issues tab
+- API for milestones, keys
+- restyled buttons
+- OAuth
+- Comment order changed
+
+## 2.8.1
+
+- ability to disable gravatars
+- improved MR diff logic
+- ssh key help page
+
+## 2.8.0
+
+- Gitlab Flavored Markdown
+- Bulk issues update
+- Issues API
+- Cucumber coverage increased
+- Post-receive files fixed
+- UI improved
+- Application cleanup
+- more cucumber
+- capybara-webkit + headless
+
+## 2.7.0
+
+- Issue Labels
+- Inline diff
+- Git HTTP
+- API
+- UI improved
+- System hooks
+- UI improved
+- Dashboard events endless scroll
+- Source performance increased
+
+## 2.6.0
+
+- UI polished
+- Improved network graph + keyboard nav
+- Handle huge commits
+- Last Push widget
+- Bugfix
+- Better performance
+- Email in resque
+- Increased test coverage
+- Ability to remove branch with MR accept
+- a lot of code refactored
+
+## 2.5.0
+
+- UI polished
+- Git blame for file
+- Bugfix
+- Email in resque
+- Better test coverage
+
+## 2.4.0
+
+- Admin area stats page
+- Ability to block user
+- Simplified dashboard area
+- Improved admin area
+- Bootstrap 2.0
+- Responsive layout
+- Big commits handling
+- Performance improved
+- Milestones
+
+## 2.3.1
+
+- Issues pagination
+- ssl fixes
+- Merge Request pagination
+
+## 2.3.0
+
+- Dashboard r1
+- Search r1
+- Project page
+- Close merge request on push
+- Persist MR diff after merge
+- mysql support
+- Documentation
+
+## 2.2.0
+
+- We’ve added support of LDAP auth
+- Improved permission logic (4 roles system)
+- Protected branches (now only masters can push to protected branches)
+- Usability improved
+- twitter bootstrap integrated
+- compare view between commits
+- wiki feature
+- now you can enable/disable issues, wiki, wall features per project
+- security fixes
+- improved code browsing (ajax branch switch etc)
+- improved per-line commenting
+- git submodules displayed
+- moved to rails 3.2
+- help section improved
+
+## 2.1.0
+
+- Project tab r1
+- List branches/tags
+- per line comments
+- mass user import
+
+## 2.0.0
+
+- gitolite as main git host system
+- merge requests
+- project/repo access
+- link to commit/issue feed
+- design tab
+- improved email notifications
+- restyled dashboard
+- bugfix
+
+## 1.2.2
+
+- common config file gitlab.yml
+- issues restyle
+- snippets restyle
+- clickable news feed header on dashboard
+- bugfix
+
+## 1.2.1
+
+- bugfix
+
+## 1.2.0
+
+- new design
+- user dashboard
+- network graph
+- markdown support for comments
+- encoding issues
+- wall like twitter timeline
+
+## 1.1.0
+
+- project dashboard
+- wall redesigned
+- feature: code snippets
+- fixed horizontal scroll on file preview
+- fixed app crash if commit message has invalid chars
+- bugfix & code cleaning
+
+## 1.0.2
+
+- fixed bug with empty project
+- added adv validation for project path & code
+- feature: issues can be sortable
+- bugfix
+- username displayed on top panel
+
+## 1.0.1
+
+- fixed: with invalid source code for commit
+- fixed: lose branch/tag selection when use tree navigation
+- when history clicked - display path
+- bug fix & code cleaning
+
+## 1.0.0
+
+- bug fix
+- projects preview mode
+
+## 0.9.6
+
+- css fix
+- new repo empty tree until restart server - fixed
+
+## 0.9.4
+
+- security improved
+- authorization improved
+- html escaping
+- bug fix
+- increased test coverage
+- design improvements
+
+## 0.9.1
+
+- increased test coverage
+- design improvements
+- new issue email notification
+- updated app name
+- issue redesigned
+- issue can be edit
+
+## 0.8.0
+
+- syntax highlight for main file types
+- redesign
+- stability
+- security fixes
+- increased test coverage
+- email notification
diff --git a/changelogs/unreleased/.gitkeep b/changelogs/unreleased/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/changelogs/unreleased/.gitkeep
diff --git a/config/application.rb b/config/application.rb
index 4792f6670a8..5dbe5a8120b 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -99,13 +99,24 @@ module Gitlab
config.action_view.sanitized_allowed_protocols = %w(smb)
- config.middleware.use Rack::Attack
+ config.middleware.insert_before Warden::Manager, Rack::Attack
# Allow access to GitLab API from other domains
- config.middleware.use Rack::Cors do
+ config.middleware.insert_before Warden::Manager, Rack::Cors do
+ allow do
+ origins Gitlab.config.gitlab.url
+ resource '/api/*',
+ credentials: true,
+ headers: :any,
+ methods: :any,
+ expose: ['Link']
+ end
+
+ # Cross-origin requests must not have the session cookie available
allow do
origins '*'
resource '/api/*',
+ credentials: false,
headers: :any,
methods: :any,
expose: ['Link']
@@ -116,6 +127,10 @@ module Gitlab
redis_config_hash = Gitlab::Redis.params
redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE
redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
+ if Sidekiq.server? # threaded context
+ redis_config_hash[:pool_size] = Sidekiq.options[:concurrency] + 5
+ redis_config_hash[:pool_timeout] = 1
+ end
config.cache_store = :redis_store, redis_config_hash
config.active_record.raise_in_transactional_callbacks = true
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 4a01b9e40fb..195108b921b 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -299,6 +299,9 @@ Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpire
Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
+Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *'
+Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
#
# GitLab Shell
diff --git a/config/initializers/7_redis.rb b/config/initializers/7_redis.rb
new file mode 100644
index 00000000000..ae2ca258df1
--- /dev/null
+++ b/config/initializers/7_redis.rb
@@ -0,0 +1,3 @@
+# Make sure we initialize a Redis connection pool before Sidekiq starts
+# multi-threaded execution.
+Gitlab::Redis.with { nil }
diff --git a/config/initializers/attr_encrypted_no_db_connection.rb b/config/initializers/attr_encrypted_no_db_connection.rb
index c668864089b..e007666b852 100644
--- a/config/initializers/attr_encrypted_no_db_connection.rb
+++ b/config/initializers/attr_encrypted_no_db_connection.rb
@@ -1,20 +1,21 @@
module AttrEncrypted
module Adapters
module ActiveRecord
- def attribute_instance_methods_as_symbols_with_no_db_connection
- # Use with_connection so the connection doesn't stay pinned to the thread.
- connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false
-
- if connected
- # Call version from AttrEncrypted::Adapters::ActiveRecord
- attribute_instance_methods_as_symbols_without_no_db_connection
- else
- # Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord
- AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call
+ module DBConnectionQuerier
+ def attribute_instance_methods_as_symbols
+ # Use with_connection so the connection doesn't stay pinned to the thread.
+ connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false
+
+ if connected
+ # Call version from AttrEncrypted::Adapters::ActiveRecord
+ super
+ else
+ # Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord
+ AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call
+ end
end
end
-
- alias_method_chain :attribute_instance_methods_as_symbols, :no_db_connection
+ prepend DBConnectionQuerier
end
end
end
diff --git a/config/initializers/connection_fix.rb b/config/initializers/connection_fix.rb
index d831a1838ed..d0b1444f607 100644
--- a/config/initializers/connection_fix.rb
+++ b/config/initializers/connection_fix.rb
@@ -20,7 +20,7 @@ if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
execute_without_retry(*args)
rescue ActiveRecord::StatementInvalid => e
if e.message =~ /server has gone away/i
- warn "Server timed out, retrying"
+ warn "Lost connection to MySQL server during query"
reconnect!
retry
else
diff --git a/config/initializers/gitlab_workhorse_secret.rb b/config/initializers/gitlab_workhorse_secret.rb
new file mode 100644
index 00000000000..ed54dc11098
--- /dev/null
+++ b/config/initializers/gitlab_workhorse_secret.rb
@@ -0,0 +1,8 @@
+begin
+ Gitlab::Workhorse.secret
+rescue
+ Gitlab::Workhorse.write_secret
+end
+
+# Try a second time. If it does not work this will raise.
+Gitlab::Workhorse.secret
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index 52522e099e7..be22085b0df 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -68,7 +68,8 @@ if Gitlab::Metrics.enabled?
['app', 'mailers', 'emails'] => ['app', 'mailers'],
['app', 'services', '**'] => ['app', 'services'],
['lib', 'gitlab', 'diff'] => ['lib'],
- ['lib', 'gitlab', 'email', 'message'] => ['lib']
+ ['lib', 'gitlab', 'email', 'message'] => ['lib'],
+ ['lib', 'gitlab', 'checks'] => ['lib']
}
paths_to_instrument.each do |(path, prefix)|
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
index f498732feca..5e3e4c966cb 100644
--- a/config/initializers/mime_types.rb
+++ b/config/initializers/mime_types.rb
@@ -13,9 +13,5 @@ Mime::Type.register "video/mp4", :mp4, [], [:m4v, :mov]
Mime::Type.register "video/webm", :webm
Mime::Type.register "video/ogg", :ogv
-middlewares = Gitlab::Application.config.middleware
-middlewares.swap(ActionDispatch::ParamsParser, ActionDispatch::ParamsParser, {
- Mime::Type.lookup('application/vnd.git-lfs+json') => lambda do |body|
- ActiveSupport::JSON.decode(body)
- end
-})
+Mime::Type.unregister :json
+Mime::Type.register 'application/json', :json, %w(application/vnd.git-lfs+json application/json)
diff --git a/config/initializers/postgresql_limit_fix.rb b/config/initializers/postgresql_limit_fix.rb
index 0cb3aaf4d24..4224d857e8a 100644
--- a/config/initializers/postgresql_limit_fix.rb
+++ b/config/initializers/postgresql_limit_fix.rb
@@ -1,5 +1,19 @@
if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
+ module LimitFilter
+ def add_column(table_name, column_name, type, options = {})
+ options.delete(:limit) if type == :text
+ super(table_name, column_name, type, options)
+ end
+
+ def change_column(table_name, column_name, type, options = {})
+ options.delete(:limit) if type == :text
+ super(table_name, column_name, type, options)
+ end
+ end
+
+ prepend ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::LimitFilter
+
class TableDefinition
def text(*args)
options = args.extract_options!
@@ -9,18 +23,5 @@ if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
column_names.each { |name| column(name, type, options) }
end
end
-
- def add_column_with_limit_filter(table_name, column_name, type, options = {})
- options.delete(:limit) if type == :text
- add_column_without_limit_filter(table_name, column_name, type, options)
- end
-
- def change_column_with_limit_filter(table_name, column_name, type, options = {})
- options.delete(:limit) if type == :text
- change_column_without_limit_filter(table_name, column_name, type, options)
- end
-
- alias_method_chain :add_column, :limit_filter
- alias_method_chain :change_column, :limit_filter
end
end
diff --git a/config/routes.rb b/config/routes.rb
index 262a174437a..ba3864b92be 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -35,6 +35,10 @@ Rails.application.routes.draw do
post :approve_access_request, on: :member
end
+ concern :awardable do
+ post :toggle_award_emoji, on: :member
+ end
+
namespace :ci do
# CI API
Ci::API::API.logger Rails.logger
@@ -98,7 +102,7 @@ Rails.application.routes.draw do
#
# Global snippets
#
- resources :snippets do
+ resources :snippets, concerns: :awardable do
member do
get 'raw'
end
@@ -110,7 +114,6 @@ Rails.application.routes.draw do
#
# Invites
#
-
resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do
member do
post :accept
@@ -632,6 +635,7 @@ Rails.application.routes.draw do
member do
get :branches
get :builds
+ get :pipelines
post :cancel_builds
post :retry_builds
post :revert
@@ -662,7 +666,7 @@ Rails.application.routes.draw do
end
end
- resources :snippets, constraints: { id: /\d+/ } do
+ resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
end
@@ -724,7 +728,7 @@ Rails.application.routes.draw do
end
end
- resources :merge_requests, constraints: { id: /\d+/ } do
+ resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get :commits
get :diffs
@@ -736,7 +740,6 @@ Rails.application.routes.draw do
post :cancel_merge_when_build_succeeds
get :ci_status
post :toggle_subscription
- post :toggle_award_emoji
post :remove_wip
get :diff_for_path
post :resolve_conflicts
@@ -747,6 +750,7 @@ Rails.application.routes.draw do
get :branch_to
get :update_branches
get :diff_for_path
+ post :bulk_update
end
resources :discussions, only: [], constraints: { id: /\h{40}/ } do
@@ -779,6 +783,8 @@ Rails.application.routes.draw do
resources :environments
+ resource :cycle_analytics, only: [:show]
+
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
@@ -837,10 +843,9 @@ Rails.application.routes.draw do
end
end
- resources :issues, constraints: { id: /\d+/ } do
+ resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
- post :toggle_award_emoji
post :mark_as_spam
get :referenced_merge_requests
get :related_branches
@@ -868,9 +873,8 @@ Rails.application.routes.draw do
resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
- resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
+ resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
- post :toggle_award_emoji
delete :delete_attachment
post :resolve
delete :resolve, action: :unresolve
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index e3316ecdb6c..a984eda5ab5 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -3,11 +3,11 @@ require 'sidekiq/testing'
Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
project_urls = [
- 'https://github.com/documentcloud/underscore.git',
+ 'https://gitlab.com/gitlab-org/gitlab-test.git',
'https://gitlab.com/gitlab-org/gitlab-ce.git',
'https://gitlab.com/gitlab-org/gitlab-ci.git',
'https://gitlab.com/gitlab-org/gitlab-shell.git',
- 'https://gitlab.com/gitlab-org/gitlab-test.git',
+ 'https://github.com/documentcloud/underscore.git',
'https://github.com/twitter/flight.git',
'https://github.com/twitter/typeahead.js.git',
'https://github.com/h5bp/html5-boilerplate.git',
@@ -38,12 +38,7 @@ Sidekiq::Testing.inline! do
]
# You can specify how many projects you need during seed execution
- size = if ENV['SIZE'].present?
- ENV['SIZE'].to_i
- else
- 8
- end
-
+ size = ENV['SIZE'].present? ? ENV['SIZE'].to_i : 8
project_urls.first(size).each_with_index do |url, i|
group_path, project_path = url.split('/')[-2..-1]
diff --git a/db/fixtures/development/06_teams.rb b/db/fixtures/development/06_teams.rb
index 3e8cdcd67b4..9739a5ac8d5 100644
--- a/db/fixtures/development/06_teams.rb
+++ b/db/fixtures/development/06_teams.rb
@@ -1,7 +1,7 @@
Gitlab::Seeder.quiet do
Group.all.each do |group|
User.all.sample(4).each do |user|
- if group.add_users([user.id], Gitlab::Access.values.sample)
+ if group.add_user(user, Gitlab::Access.values.sample).persisted?
print '.'
else
print 'F'
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 49e6e2361b1..650b410595c 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -3,9 +3,13 @@ class Gitlab::Seeder::Pipelines
BUILDS = [
{ name: 'build:linux', stage: 'build', status: :success },
{ name: 'build:osx', stage: 'build', status: :success },
- { name: 'rspec:linux', stage: 'test', status: :success },
- { name: 'rspec:windows', stage: 'test', status: :success },
- { name: 'rspec:windows', stage: 'test', status: :success },
+ { name: 'rspec:linux 0 3', stage: 'test', status: :success },
+ { name: 'rspec:linux 1 3', stage: 'test', status: :success },
+ { name: 'rspec:linux 2 3', stage: 'test', status: :success },
+ { name: 'rspec:windows 0 3', stage: 'test', status: :success },
+ { name: 'rspec:windows 1 3', stage: 'test', status: :success },
+ { name: 'rspec:windows 2 3', stage: 'test', status: :success },
+ { name: 'rspec:windows 2 3', stage: 'test', status: :success },
{ name: 'rspec:osx', stage: 'test', status_event: :success },
{ name: 'spinach:linux', stage: 'test', status: :success },
{ name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true},
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
new file mode 100644
index 00000000000..e882a492757
--- /dev/null
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -0,0 +1,246 @@
+require 'sidekiq/testing'
+require './spec/support/test_env'
+
+class Gitlab::Seeder::CycleAnalytics
+ def initialize(project, perf: false)
+ @project = project
+ @user = User.order(:id).last
+ @issue_count = perf ? 1000 : 5
+ stub_git_pre_receive!
+ end
+
+ # The GitLab API needn't be running for the fixtures to be
+ # created. Since we're performing a number of git actions
+ # here (like creating a branch or committing a file), we need
+ # to disable the `pre_receive` hook in order to remove this
+ # dependency on the GitLab API.
+ def stub_git_pre_receive!
+ GitHooksService.class_eval do
+ def run_hook(name)
+ [true, '']
+ end
+ end
+ end
+
+ def seed_metrics!
+ @issue_count.times do |index|
+ # Issue
+ Timecop.travel 5.days.from_now
+ title = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+ issue = Issue.create(project: @project, title: title, author: @user)
+ issue_metrics = issue.metrics
+
+ # Milestones / Labels
+ Timecop.travel 5.days.from_now
+ if index.even?
+ issue_metrics.first_associated_with_milestone_at = rand(6..12).hours.from_now
+ else
+ issue_metrics.first_added_to_board_at = rand(6..12).hours.from_now
+ end
+
+ # Commit
+ Timecop.travel 5.days.from_now
+ issue_metrics.first_mentioned_in_commit_at = rand(6..12).hours.from_now
+
+ # MR
+ Timecop.travel 5.days.from_now
+ branch_name = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+ @project.repository.add_branch(@user, branch_name, 'master')
+ merge_request = MergeRequest.create(target_project: @project, source_project: @project, source_branch: branch_name, target_branch: 'master', title: branch_name, author: @user)
+ merge_request_metrics = merge_request.metrics
+
+ # MR closing issues
+ Timecop.travel 5.days.from_now
+ MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request)
+
+ # Merge
+ Timecop.travel 5.days.from_now
+ merge_request_metrics.merged_at = rand(6..12).hours.from_now
+
+ # Start build
+ Timecop.travel 5.days.from_now
+ merge_request_metrics.latest_build_started_at = rand(6..12).hours.from_now
+
+ # Finish build
+ Timecop.travel 5.days.from_now
+ merge_request_metrics.latest_build_finished_at = rand(6..12).hours.from_now
+
+ # Deploy to production
+ Timecop.travel 5.days.from_now
+ merge_request_metrics.first_deployed_to_production_at = rand(6..12).hours.from_now
+
+ issue_metrics.save!
+ merge_request_metrics.save!
+
+ print '.'
+ end
+ end
+
+ def seed!
+ Sidekiq::Testing.inline! do
+ issues = create_issues
+ puts '.'
+
+ # Stage 1
+ Timecop.travel 5.days.from_now
+ add_milestones_and_list_labels(issues)
+ print '.'
+
+ # Stage 2
+ Timecop.travel 5.days.from_now
+ branches = mention_in_commits(issues)
+ print '.'
+
+ # Stage 3
+ Timecop.travel 5.days.from_now
+ merge_requests = create_merge_requests_closing_issues(issues, branches)
+ print '.'
+
+ # Stage 4
+ Timecop.travel 5.days.from_now
+ run_builds(merge_requests)
+ print '.'
+
+ # Stage 5
+ Timecop.travel 5.days.from_now
+ merge_merge_requests(merge_requests)
+ print '.'
+
+ # Stage 6 / 7
+ Timecop.travel 5.days.from_now
+ deploy_to_production(merge_requests)
+ print '.'
+ end
+
+ print '.'
+ end
+
+ private
+
+ def create_issues
+ Array.new(@issue_count) do
+ issue_params = {
+ title: "Cycle Analytics: #{FFaker::Lorem.sentence(6)}",
+ description: FFaker::Lorem.sentence,
+ state: 'opened',
+ assignee: @project.team.users.sample
+ }
+
+ Issues::CreateService.new(@project, @project.team.users.sample, issue_params).execute
+ end
+ end
+
+ def add_milestones_and_list_labels(issues)
+ issues.shuffle.map.with_index do |issue, index|
+ Timecop.travel 12.hours.from_now
+
+ if index.even?
+ issue.update(milestone: @project.milestones.sample)
+ else
+ label_name = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+ list_label = FactoryGirl.create(:label, title: label_name, project: issue.project)
+ FactoryGirl.create(:list, board: FactoryGirl.create(:board, project: issue.project), label: list_label)
+ issue.update(labels: [list_label])
+ end
+
+ issue
+ end
+ end
+
+ def mention_in_commits(issues)
+ issues.map do |issue|
+ Timecop.travel 12.hours.from_now
+
+ branch_name = filename = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+
+ issue.project.repository.add_branch(@user, branch_name, 'master')
+
+ options = {
+ committer: issue.project.repository.user_to_committer(@user),
+ author: issue.project.repository.user_to_committer(@user),
+ commit: { message: "Commit for ##{issue.iid}", branch: branch_name, update_ref: true },
+ file: { content: "content", path: filename, update: false }
+ }
+
+ commit_sha = Gitlab::Git::Blob.commit(issue.project.repository, options)
+ issue.project.repository.commit(commit_sha)
+
+
+ GitPushService.new(issue.project,
+ @user,
+ oldrev: issue.project.repository.commit("master").sha,
+ newrev: commit_sha,
+ ref: 'refs/heads/master').execute
+
+ branch_name
+ end
+ end
+
+ def create_merge_requests_closing_issues(issues, branches)
+ issues.zip(branches).map do |issue, branch|
+ Timecop.travel 12.hours.from_now
+
+ opts = {
+ title: 'Cycle Analytics merge_request',
+ description: "Fixes #{issue.to_reference}",
+ source_branch: branch,
+ target_branch: 'master'
+ }
+
+ MergeRequests::CreateService.new(issue.project, @user, opts).execute
+ end
+ end
+
+ def run_builds(merge_requests)
+ merge_requests.each do |merge_request|
+ Timecop.travel 12.hours.from_now
+
+ service = Ci::CreatePipelineService.new(merge_request.project,
+ @user,
+ ref: "refs/heads/#{merge_request.source_branch}")
+ pipeline = service.execute(ignore_skip_ci: true, save_on_errors: false)
+
+ pipeline.run!
+ Timecop.travel rand(1..6).hours.from_now
+ pipeline.succeed!
+ end
+ end
+
+ def merge_merge_requests(merge_requests)
+ merge_requests.each do |merge_request|
+ Timecop.travel 12.hours.from_now
+
+ MergeRequests::MergeService.new(merge_request.project, @user).execute(merge_request)
+ end
+ end
+
+ def deploy_to_production(merge_requests)
+ merge_requests.each do |merge_request|
+ Timecop.travel 12.hours.from_now
+
+ CreateDeploymentService.new(merge_request.project, @user, {
+ environment: 'production',
+ ref: 'master',
+ tag: false,
+ sha: @project.repository.commit('master').sha
+ }).execute
+ end
+ end
+end
+
+Gitlab::Seeder.quiet do
+ if ENV['SEED_CYCLE_ANALYTICS']
+ Project.all.each do |project|
+ seeder = Gitlab::Seeder::CycleAnalytics.new(project)
+ seeder.seed!
+ end
+ elsif ENV['CYCLE_ANALYTICS_PERF_TEST']
+ seeder = Gitlab::Seeder::CycleAnalytics.new(Project.order(:id).first, perf: true)
+ seeder.seed!
+ elsif ENV['CYCLE_ANALYTICS_POPULATE_METRICS_DIRECTLY']
+ seeder = Gitlab::Seeder::CycleAnalytics.new(Project.order(:id).first, perf: true)
+ seeder.seed_metrics!
+ else
+ puts "Not running the cycle analytics seed file. Use the `SEED_CYCLE_ANALYTICS` environment variable to enable it."
+ end
+end
diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb
index 84463727b3b..e8de7ccf3db 100644
--- a/db/migrate/20140502125220_migrate_repo_size.rb
+++ b/db/migrate/20140502125220_migrate_repo_size.rb
@@ -1,12 +1,15 @@
# rubocop:disable all
class MigrateRepoSize < ActiveRecord::Migration
+ DOWNTIME = false
+
def up
project_data = execute('SELECT projects.id, namespaces.path AS namespace_path, projects.path AS project_path FROM projects LEFT JOIN namespaces ON projects.namespace_id = namespaces.id')
project_data.each do |project|
id = project['id']
namespace_path = project['namespace_path'] || ''
- path = File.join(Gitlab.config.gitlab_shell.repos_path, namespace_path, project['project_path'] + '.git')
+ repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default
+ path = File.join(repos_path, namespace_path, project['project_path'] + '.git')
begin
repo = Gitlab::Git::Repository.new(path)
diff --git a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
index c8cbd2718ff..75a3eb15124 100644
--- a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
+++ b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
@@ -8,14 +8,28 @@ class MergeRequestDiffRemoveUniq < ActiveRecord::Migration
DOWNTIME = false
def up
- if index_exists?(:merge_request_diffs, :merge_request_id)
- remove_index :merge_request_diffs, :merge_request_id
+ constraint_name = 'merge_request_diffs_merge_request_id_key'
+
+ transaction do
+ if index_exists?(:merge_request_diffs, :merge_request_id)
+ remove_index(:merge_request_diffs, :merge_request_id)
+ end
+
+ # In some bizarre cases PostgreSQL might have a separate unique constraint
+ # that we'll need to drop.
+ if constraint_exists?(constraint_name) && Gitlab::Database.postgresql?
+ execute("ALTER TABLE merge_request_diffs DROP CONSTRAINT IF EXISTS #{constraint_name};")
+ end
end
end
def down
unless index_exists?(:merge_request_diffs, :merge_request_id)
- add_concurrent_index :merge_request_diffs, :merge_request_id, unique: true
+ add_concurrent_index(:merge_request_diffs, :merge_request_id, unique: true)
end
end
+
+ def constraint_exists?(name)
+ indexes(:merge_request_diffs).map(&:name).include?(name)
+ end
end
diff --git a/db/migrate/20160808085531_add_token_to_build.rb b/db/migrate/20160808085531_add_token_to_build.rb
new file mode 100644
index 00000000000..3ed2a103ae3
--- /dev/null
+++ b/db/migrate/20160808085531_add_token_to_build.rb
@@ -0,0 +1,10 @@
+class AddTokenToBuild < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds, :token, :string
+ end
+end
diff --git a/db/migrate/20160808085602_add_index_for_build_token.rb b/db/migrate/20160808085602_add_index_for_build_token.rb
new file mode 100644
index 00000000000..10ef42afce1
--- /dev/null
+++ b/db/migrate/20160808085602_add_index_for_build_token.rb
@@ -0,0 +1,12 @@
+class AddIndexForBuildToken < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :ci_builds, :token, unique: true
+ end
+end
diff --git a/db/migrate/20160824124900_add_table_issue_metrics.rb b/db/migrate/20160824124900_add_table_issue_metrics.rb
new file mode 100644
index 00000000000..e9bb79b3c62
--- /dev/null
+++ b/db/migrate/20160824124900_add_table_issue_metrics.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTableIssueMetrics < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Adding foreign key'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ create_table :issue_metrics do |t|
+ t.references :issue, index: { name: "index_issue_metrics" }, foreign_key: { on_delete: :cascade }, null: false
+
+ t.datetime 'first_mentioned_in_commit_at'
+ t.datetime 'first_associated_with_milestone_at'
+ t.datetime 'first_added_to_board_at'
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160825052008_add_table_merge_request_metrics.rb b/db/migrate/20160825052008_add_table_merge_request_metrics.rb
new file mode 100644
index 00000000000..e01cc5038b9
--- /dev/null
+++ b/db/migrate/20160825052008_add_table_merge_request_metrics.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTableMergeRequestMetrics < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Adding foreign key'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ create_table :merge_request_metrics do |t|
+ t.references :merge_request, index: { name: "index_merge_request_metrics" }, foreign_key: { on_delete: :cascade }, null: false
+
+ t.datetime 'latest_build_started_at'
+ t.datetime 'latest_build_finished_at'
+ t.datetime 'first_deployed_to_production_at', index: true
+ t.datetime 'merged_at'
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb b/db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb
new file mode 100644
index 00000000000..a27947212f6
--- /dev/null
+++ b/db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb
@@ -0,0 +1,15 @@
+class AddConfidentialIssuesEventsToWebHooks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :web_hooks, :confidential_issues_events, :boolean, default: false, allow_null: false
+ end
+
+ def down
+ remove_column :web_hooks, :confidential_issues_events
+ end
+end
diff --git a/db/migrate/20160830211132_add_confidential_issues_events_to_services.rb b/db/migrate/20160830211132_add_confidential_issues_events_to_services.rb
new file mode 100644
index 00000000000..030e7c39350
--- /dev/null
+++ b/db/migrate/20160830211132_add_confidential_issues_events_to_services.rb
@@ -0,0 +1,15 @@
+class AddConfidentialIssuesEventsToServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :services, :confidential_issues_events, :boolean, default: true, allow_null: false
+ end
+
+ def down
+ remove_column :services, :confidential_issues_events
+ end
+end
diff --git a/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb
new file mode 100644
index 00000000000..f1a1f001cb3
--- /dev/null
+++ b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb
@@ -0,0 +1,15 @@
+class SetConfidentialIssuesEventsOnWebhooks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ update_column_in_batches(:web_hooks, :confidential_issues_events, true) do |table, query|
+ query.where(table[:issues_events].eq(true))
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb b/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb
new file mode 100644
index 00000000000..fd413d1ca8c
--- /dev/null
+++ b/db/migrate/20160901213340_add_lfs_enabled_to_namespaces.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLfsEnabledToNamespaces < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :namespaces, :lfs_enabled, :boolean
+ end
+end
diff --git a/db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb b/db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb
new file mode 100644
index 00000000000..a80a57254dd
--- /dev/null
+++ b/db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb
@@ -0,0 +1,39 @@
+class DropGitoriousFieldFromApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # After the deploy the caches will be cold anyway
+ DOWNTIME = false
+
+ def up
+ require 'yaml'
+
+ import_sources = connection.execute('SELECT import_sources FROM application_settings;')
+ return unless import_sources.first # support empty databases
+
+ yaml = if Gitlab::Database.postgresql?
+ import_sources.values[0][0]
+ else
+ import_sources.first[0]
+ end
+
+ yaml = YAML.safe_load(yaml)
+ yaml.delete 'gitorious'
+
+ # No need for a WHERE clause as there is only one
+ connection.execute("UPDATE application_settings SET import_sources = #{update_yaml(yaml)}")
+ end
+
+ def down
+ # noop, gitorious still yields a 404 anyway
+ end
+
+ private
+
+ def connection
+ ActiveRecord::Base.connection
+ end
+
+ def update_yaml(yaml)
+ connection.quote(YAML.dump(yaml))
+ end
+end
diff --git a/db/migrate/20160907131111_add_environment_type_to_environments.rb b/db/migrate/20160907131111_add_environment_type_to_environments.rb
new file mode 100644
index 00000000000..fac73753d5b
--- /dev/null
+++ b/db/migrate/20160907131111_add_environment_type_to_environments.rb
@@ -0,0 +1,9 @@
+class AddEnvironmentTypeToEnvironments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :environments, :environment_type, :string
+ end
+end
diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
new file mode 100644
index 00000000000..18ea9d43a43
--- /dev/null
+++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveProjectsPushesSinceGc < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration removes an existing column'
+
+ disable_ddl_transaction!
+
+ def up
+ remove_column :projects, :pushes_since_gc
+ end
+
+ def down
+ add_column_with_default :projects, :pushes_since_gc, :integer, default: 0
+ end
+end
diff --git a/db/migrate/20160913212128_change_artifacts_size_column.rb b/db/migrate/20160913212128_change_artifacts_size_column.rb
new file mode 100644
index 00000000000..063bbca537c
--- /dev/null
+++ b/db/migrate/20160913212128_change_artifacts_size_column.rb
@@ -0,0 +1,15 @@
+class ChangeArtifactsSizeColumn < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+
+ DOWNTIME_REASON = 'Changing an integer column size requires a full table rewrite.'
+
+ def up
+ change_column :ci_builds, :artifacts_size, :integer, limit: 8
+ end
+
+ def down
+ # do nothing
+ end
+end
diff --git a/db/migrate/20160915042921_create_merge_requests_closing_issues.rb b/db/migrate/20160915042921_create_merge_requests_closing_issues.rb
new file mode 100644
index 00000000000..94874a853da
--- /dev/null
+++ b/db/migrate/20160915042921_create_merge_requests_closing_issues.rb
@@ -0,0 +1,34 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateMergeRequestsClosingIssues < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Adding foreign keys'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ create_table :merge_requests_closing_issues do |t|
+ t.references :merge_request, foreign_key: { on_delete: :cascade }, index: true, null: false
+ t.references :issue, foreign_key: { on_delete: :cascade }, index: true, null: false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160920160832_add_index_to_labels_title.rb b/db/migrate/20160920160832_add_index_to_labels_title.rb
new file mode 100644
index 00000000000..b5de552b98c
--- /dev/null
+++ b/db/migrate/20160920160832_add_index_to_labels_title.rb
@@ -0,0 +1,11 @@
+class AddIndexToLabelsTitle < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :labels, :title
+ end
+end
diff --git a/db/migrate/20160926145521_add_organization_to_user.rb b/db/migrate/20160926145521_add_organization_to_user.rb
new file mode 100644
index 00000000000..e0bef6e7548
--- /dev/null
+++ b/db/migrate/20160926145521_add_organization_to_user.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddOrganizationToUser < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :users, :organization, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index af6e74a4e25..ad62c249b3f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160831223750) do
+ActiveRecord::Schema.define(version: 20160926145521) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -158,9 +158,9 @@ ActiveRecord::Schema.define(version: 20160831223750) do
t.text "commands"
t.integer "job_id"
t.string "name"
- t.boolean "deploy", default: false
+ t.boolean "deploy", default: false
t.text "options"
- t.boolean "allow_failure", default: false, null: false
+ t.boolean "allow_failure", default: false, null: false
t.string "stage"
t.integer "trigger_request_id"
t.integer "stage_idx"
@@ -177,10 +177,11 @@ ActiveRecord::Schema.define(version: 20160831223750) do
t.datetime "erased_at"
t.datetime "artifacts_expire_at"
t.string "environment"
- t.integer "artifacts_size"
+ t.integer "artifacts_size", limit: 8
t.string "when"
t.text "yaml_variables"
t.datetime "queued_at"
+ t.string "token"
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
@@ -192,6 +193,7 @@ ActiveRecord::Schema.define(version: 20160831223750) do
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", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
create_table "ci_commits", force: :cascade do |t|
t.integer "project_id"
@@ -390,10 +392,11 @@ ActiveRecord::Schema.define(version: 20160831223750) do
create_table "environments", force: :cascade do |t|
t.integer "project_id"
- t.string "name", null: false
+ t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "external_url"
+ t.string "environment_type"
end
add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
@@ -436,6 +439,17 @@ ActiveRecord::Schema.define(version: 20160831223750) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
+ create_table "issue_metrics", force: :cascade do |t|
+ t.integer "issue_id", null: false
+ t.datetime "first_mentioned_in_commit_at"
+ t.datetime "first_associated_with_milestone_at"
+ t.datetime "first_added_to_board_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "issue_metrics", ["issue_id"], name: "index_issue_metrics", using: :btree
+
create_table "issues", force: :cascade do |t|
t.string "title"
t.integer "assignee_id"
@@ -507,6 +521,7 @@ ActiveRecord::Schema.define(version: 20160831223750) do
add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
+ add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false
@@ -578,6 +593,19 @@ ActiveRecord::Schema.define(version: 20160831223750) do
add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree
+ create_table "merge_request_metrics", force: :cascade do |t|
+ t.integer "merge_request_id", null: false
+ t.datetime "latest_build_started_at"
+ t.datetime "latest_build_finished_at"
+ t.datetime "first_deployed_to_production_at"
+ t.datetime "merged_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
+ add_index "merge_request_metrics", ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
+
create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
t.string "source_branch", null: false
@@ -619,6 +647,16 @@ ActiveRecord::Schema.define(version: 20160831223750) do
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
+ create_table "merge_requests_closing_issues", force: :cascade do |t|
+ t.integer "merge_request_id", null: false
+ t.integer "issue_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "merge_requests_closing_issues", ["issue_id"], name: "index_merge_requests_closing_issues_on_issue_id", using: :btree
+ add_index "merge_requests_closing_issues", ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree
+
create_table "milestones", force: :cascade do |t|
t.string "title", null: false
t.integer "project_id", null: false
@@ -650,6 +688,7 @@ ActiveRecord::Schema.define(version: 20160831223750) do
t.integer "visibility_level", default: 20, null: false
t.boolean "request_access_enabled", default: true, null: false
t.datetime "deleted_at"
+ t.boolean "lfs_enabled"
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
@@ -824,7 +863,6 @@ ActiveRecord::Schema.define(version: 20160831223750) do
t.integer "build_timeout", default: 3600, null: false
t.boolean "pending_delete", default: false
t.boolean "public_builds", default: true, null: false
- t.integer "pushes_since_gc", default: 0
t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at"
t.boolean "container_registry_enabled"
@@ -909,19 +947,20 @@ ActiveRecord::Schema.define(version: 20160831223750) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "active", default: false, null: false
+ t.boolean "active", default: false, null: false
t.text "properties"
- t.boolean "template", default: false
- t.boolean "push_events", default: true
- t.boolean "issues_events", default: true
- t.boolean "merge_requests_events", default: true
- t.boolean "tag_push_events", default: true
- t.boolean "note_events", default: true, null: false
- t.boolean "build_events", default: false, null: false
- t.string "category", default: "common", null: false
- t.boolean "default", default: false
- t.boolean "wiki_page_events", default: true
- t.boolean "pipeline_events", default: false, null: false
+ t.boolean "template", default: false
+ t.boolean "push_events", default: true
+ t.boolean "issues_events", default: true
+ t.boolean "merge_requests_events", default: true
+ t.boolean "tag_push_events", default: true
+ t.boolean "note_events", default: true, null: false
+ t.boolean "build_events", default: false, null: false
+ t.string "category", default: "common", null: false
+ t.boolean "default", default: false
+ t.boolean "wiki_page_events", default: true
+ t.boolean "pipeline_events", default: false, null: false
+ t.boolean "confidential_issues_events", default: true, null: false
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
@@ -1093,6 +1132,7 @@ ActiveRecord::Schema.define(version: 20160831223750) do
t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false
+ t.string "organization"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -1121,29 +1161,34 @@ ActiveRecord::Schema.define(version: 20160831223750) do
add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree
create_table "web_hooks", force: :cascade do |t|
- t.string "url", limit: 2000
+ t.string "url", limit: 2000
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "type", default: "ProjectHook"
+ t.string "type", default: "ProjectHook"
t.integer "service_id"
- t.boolean "push_events", default: true, null: false
- t.boolean "issues_events", default: false, null: false
- t.boolean "merge_requests_events", default: false, null: false
- t.boolean "tag_push_events", default: false
- t.boolean "note_events", default: false, null: false
- t.boolean "enable_ssl_verification", default: true
- t.boolean "build_events", default: false, null: false
- t.boolean "wiki_page_events", default: false, null: false
+ t.boolean "push_events", default: true, null: false
+ t.boolean "issues_events", default: false, null: false
+ t.boolean "merge_requests_events", default: false, null: false
+ t.boolean "tag_push_events", default: false
+ t.boolean "note_events", default: false, null: false
+ t.boolean "enable_ssl_verification", default: true
+ t.boolean "build_events", default: false, null: false
+ t.boolean "wiki_page_events", default: false, null: false
t.string "token"
- t.boolean "pipeline_events", default: false, null: false
+ t.boolean "pipeline_events", default: false, null: false
+ t.boolean "confidential_issues_events", default: false, null: false
end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_foreign_key "boards", "projects"
+ add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels"
+ add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
+ add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
+ add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "personal_access_tokens", "users"
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
diff --git a/doc/README.md b/doc/README.md
index 254394eb63e..4ff1a0582c8 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -19,6 +19,7 @@
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
+- [University](university/README.md) Learn Git and GitLab through videos and courses.
## Administrator documentation
@@ -29,9 +30,9 @@
- [Install](install/README.md) Requirements, directory structures and installation from source.
- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components.
- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
-- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
+- [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages.
- [Koding](administration/integration/koding.md) Set up Koding to use with GitLab.
-- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
+- [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars.
- [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running.
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 7186f707ad6..bf7814875bf 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -275,3 +275,9 @@ If you are getting 'Connection Refused' errors when trying to connect to the
LDAP server please double-check the LDAP `port` and `method` settings used by
GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR
`method: 'ssl'` and `port: 636`.
+
+### Login with valid credentials rejected
+
+If there is an unexpected error while authenticating the user with the LDAP
+backend, the login is rejected and details about the error are logged to
+`production.log`.
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 28c4c7c86ca..c5611e2a121 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -406,7 +406,8 @@ To configure the storage driver in Omnibus:
's3' => {
'accesskey' => 's3-access-key',
'secretkey' => 's3-secret-key-for-access-key',
- 'bucket' => 'your-s3-bucket'
+ 'bucket' => 'your-s3-bucket',
+ 'region' => 'your-s3-region'
}
}
```
@@ -428,6 +429,7 @@ storage:
accesskey: 'AKIAKIAKI'
secretkey: 'secret123'
bucket: 'gitlab-registry-bucket-AKIAKIAKI'
+ region: 'your-s3-region'
cache:
blobdescriptor: inmemory
delete:
diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md
new file mode 100644
index 00000000000..28e1fd4e12e
--- /dev/null
+++ b/doc/administration/issue_closing_pattern.md
@@ -0,0 +1,49 @@
+# Issue closing pattern
+
+>**Note:**
+This is the administration documentation.
+There is a separate [user documentation] on issue closing pattern.
+
+When a commit or merge request resolves one or more issues, it is possible to
+automatically have these issues closed when the commit or merge request lands
+in the project's default branch.
+
+## Change the issue closing pattern
+
+In order to change the pattern you need to have access to the server that GitLab
+is installed on.
+
+The default pattern can be located in [gitlab.yml.example] under the
+"Automatic issue closing" section.
+
+> **Tip:**
+You are advised to use http://rubular.com to test the issue closing pattern.
+Because Rubular doesn't understand `%{issue_ref}`, you can replace this by
+`#\d+` when testing your patterns, which matches only local issue references like `#123`.
+
+**For Omnibus installations**
+
+1. Open `/etc/gitlab/gitlab.rb` with your editor.
+1. Change the value of `gitlab_rails['issue_closing_pattern']` to a regular
+ expression of your liking:
+
+ ```ruby
+ gitlab_rails['issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
+ ```
+1. [Reconfigure] GitLab for the changes to take effect.
+
+**For installations from source**
+
+1. Open `gitlab.yml` with your editor.
+1. Change the value of `issue_closing_pattern`:
+
+ ```yaml
+ issue_closing_pattern: "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
+ ```
+
+1. [Restart] GitLab for the changes to take effect.
+
+[gitlab.yml.example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example
+[reconfigure]: restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: restart_gitlab.md#installations-from-source
+[user documentation]: ../user/project/issues/automatic_issue_closing.md
diff --git a/doc/api/README.md b/doc/api/README.md
index 3e79cce0120..bbd5bcfb386 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -10,10 +10,12 @@ following locations:
- [Award Emoji](award_emoji.md)
- [Branches](branches.md)
+- [Broadcast Messages](broadcast_messages.md)
- [Builds](builds.md)
-- [Build triggers](build_triggers.md)
+- [Build Triggers](build_triggers.md)
- [Build Variables](build_variables.md)
- [Commits](commits.md)
+- [Deployments](deployments.md)
- [Deploy Keys](deploy_keys.md)
- [Groups](groups.md)
- [Group Access Requests](access_requests.md)
@@ -26,6 +28,7 @@ following locations:
- [Open source license templates](licenses.md)
- [Namespaces](namespaces.md)
- [Notes](notes.md) (comments)
+- [Notification settings](notification_settings.md)
- [Pipelines](pipelines.md)
- [Projects](projects.md) including setting Webhooks
- [Project Access Requests](access_requests.md)
@@ -40,8 +43,9 @@ following locations:
- [Sidekiq metrics](sidekiq_metrics.md)
- [System Hooks](system_hooks.md)
- [Tags](tags.md)
-- [Users](users.md)
- [Todos](todos.md)
+- [Users](users.md)
+- [Validate CI configuration](ci/lint.md)
### Internal CI API
@@ -52,11 +56,12 @@ The following documentation is for the [internal CI API](ci/README.md):
## Authentication
-All API requests require authentication via a token. There are three types of tokens
-available: private tokens, OAuth 2 tokens, and personal access tokens.
+All API requests require authentication via a session cookie or token. There are
+three types of tokens available: private tokens, OAuth 2 tokens, and personal
+access tokens.
-If a token is invalid or omitted, an error message will be returned with
-status code `401`:
+If authentication information is invalid or omitted, an error message will be
+returned with status code `401`:
```json
{
@@ -95,6 +100,13 @@ that needs access to the GitLab API.
Once you have your token, pass it to the API using either the `private_token`
parameter or the `PRIVATE-TOKEN` header.
+
+### Session Cookie
+
+When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is
+set. The API will use this cookie for authentication if it is present, but using
+the API to generate a new session cookie is currently not supported.
+
## Basic Usage
API requests should be prefixed with `api` and the API version. The API version
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index 72ec99b7c56..c464e3f3f71 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -1,12 +1,13 @@
# Award Emoji
-> [Introduced][ce-4575] in GitLab 8.9.
+> [Introduced][ce-4575] in GitLab 8.9, Snippet support in 8.12
+
An awarded emoji tells a thousand words, and can be awarded on issues, merge
-requests and notes/comments. Issues, merge requests and notes are further called
+requests, snippets, and notes/comments. Issues, merge requests, snippets, and notes are further called
`awardables`.
-## Issues and merge requests
+## Issues, merge requests, and snippets
### List an awardable's award emoji
@@ -15,6 +16,7 @@ Gets a list of all award emoji
```
GET /projects/:id/issues/:issue_id/award_emoji
GET /projects/:id/merge_requests/:merge_request_id/award_emoji
+GET /projects/:id/snippets/:snippet_id/award_emoji
```
Parameters:
@@ -69,11 +71,12 @@ Example Response:
### Get single award emoji
-Gets a single award emoji from an issue or merge request.
+Gets a single award emoji from an issue, snippet, or merge request.
```
GET /projects/:id/issues/:issue_id/award_emoji/:award_id
GET /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id
```
Parameters:
@@ -116,6 +119,7 @@ This end point creates an award emoji on the specified resource
```
POST /projects/:id/issues/:issue_id/award_emoji
POST /projects/:id/merge_requests/:merge_request_id/award_emoji
+POST /projects/:id/snippets/:snippet_id/award_emoji
```
Parameters:
@@ -159,6 +163,7 @@ admins or the author of the award. Status code 200 on success, 401 if unauthoriz
```
DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id
DELETE /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id
```
Parameters:
@@ -197,7 +202,7 @@ Example Response:
## Award Emoji on Notes
The endpoints documented above are available for Notes as well. Notes
-are a sub-resource of Issues and Merge Requests. The examples below
+are a sub-resource of Issues, Merge Requests, or Snippets. The examples below
describe working with Award Emoji on notes for an Issue, but can be
easily adapted for notes on a Merge Request.
diff --git a/doc/api/builds.md b/doc/api/builds.md
index dce666445d0..e8a9e4743d3 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -40,6 +40,12 @@ Example of response
"finished_at": "2015-12-24T17:54:27.895Z",
"id": 7,
"name": "teaspoon",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ }
"ref": "master",
"runner": null,
"stage": "test",
@@ -78,6 +84,12 @@ Example of response
"finished_at": "2015-12-24T17:54:24.921Z",
"id": 6,
"name": "spinach:other",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ }
"ref": "master",
"runner": null,
"stage": "test",
@@ -146,6 +158,12 @@ Example of response
"finished_at": "2016-01-11T10:14:09.526Z",
"id": 69,
"name": "rubocop",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ }
"ref": "master",
"runner": null,
"stage": "test",
@@ -170,6 +188,12 @@ Example of response
"finished_at": "2015-12-24T17:54:33.913Z",
"id": 9,
"name": "brakeman",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ }
"ref": "master",
"runner": null,
"stage": "test",
@@ -231,6 +255,12 @@ Example of response
"finished_at": "2015-12-24T17:54:31.198Z",
"id": 8,
"name": "rubocop",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ }
"ref": "master",
"runner": null,
"stage": "test",
diff --git a/doc/api/ci/builds.md b/doc/api/ci/builds.md
index 2a71b087f19..b6d79706a84 100644
--- a/doc/api/ci/builds.md
+++ b/doc/api/ci/builds.md
@@ -38,6 +38,15 @@ POST /ci/api/v1/builds/register
curl --request POST "https://gitlab.example.com/ci/api/v1/builds/register" --form "token=t0k3n"
```
+**Responses:**
+
+| Status | Data |Description |
+|--------|------|---------------------------------------------------------------------------|
+| `201` | yes | When a build is scheduled for a runner |
+| `204` | no | When no builds are scheduled for a runner (for GitLab Runner >= `v1.3.0`) |
+| `403` | no | When invalid token is used or no token is sent |
+| `404` | no | When no builds are scheduled for a runner (for GitLab Runner < `v1.3.0`) **or** when the runner is set to `paused` in GitLab runner's configuration page |
+
### Update details of an existing build
```
diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md
new file mode 100644
index 00000000000..0c96b3ee335
--- /dev/null
+++ b/doc/api/ci/lint.md
@@ -0,0 +1,49 @@
+# Validate the .gitlab-ci.yml
+
+> [Introduced][ce-5953] in GitLab 8.12.
+
+Checks if your .gitlab-ci.yml file is valid.
+
+```
+POST ci/lint
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ------- | -------- | -------- |
+| `content` | string | yes | the .gitlab-ci.yaml content|
+
+```bash
+curl --header "Content-Type: application/json" https://gitlab.example.com/api/v3/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}'
+```
+
+Be sure to copy paste the exact contents of `.gitlab-ci.yml` as YAML is very picky about indentation and spaces.
+
+Example responses:
+
+* Valid content:
+
+ ```json
+ {
+ "status": "valid",
+ "errors": []
+ }
+ ```
+
+* Invalid content:
+
+ ```json
+ {
+ "status": "invalid",
+ "errors": [
+ "variables config should be a hash of key value pairs"
+ ]
+ }
+ ```
+
+* Without the content attribute:
+
+ ```json
+ {
+ "error": "content is missing"
+ }
+ ```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index a898387eaa2..e81d6f9de4b 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -84,7 +84,8 @@ Parameters:
"forks_count": 0,
"open_issues_count": 3,
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "request_access_enabled": false
}
]
```
@@ -118,6 +119,7 @@ Example response:
"visibility_level": 20,
"avatar_url": null,
"web_url": "https://gitlab.example.com/groups/twitter",
+ "request_access_enabled": false,
"projects": [
{
"id": 7,
@@ -163,7 +165,8 @@ Example response:
"forks_count": 0,
"open_issues_count": 3,
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "request_access_enabled": false
},
{
"id": 6,
@@ -209,7 +212,8 @@ Example response:
"forks_count": 0,
"open_issues_count": 8,
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "request_access_enabled": false
}
],
"shared_projects": [
@@ -288,6 +292,8 @@ Parameters:
- `path` (required) - The path of the group
- `description` (optional) - The group's description
- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
+- `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group
+- `request_access_enabled` (optional) - Allow users to request member access.
## Transfer project to group
@@ -317,6 +323,8 @@ PUT /groups/:id
| `path` | string | no | The path of the group |
| `description` | string | no | The description of the group |
| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. |
+| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group |
+| `request_access_enabled` | boolean | no | Allow users to request member access. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental"
@@ -334,6 +342,7 @@ Example response:
"visibility_level": 10,
"avatar_url": null,
"web_url": "http://gitlab.example.com/groups/h5bp",
+ "request_access_enabled": false,
"projects": [
{
"id": 9,
@@ -378,7 +387,8 @@ Example response:
"forks_count": 0,
"open_issues_count": 3,
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "request_access_enabled": false
}
]
}
diff --git a/doc/api/members.md b/doc/api/members.md
index fd6d728dad2..6535e9a7801 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -110,8 +110,8 @@ POST /projects/:id/members
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=30
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/groups/:id/members
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/projects/:id/members
```
Example response:
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 85d140d06ac..572844b8b3f 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -78,7 +78,8 @@ Parameters:
### Create new issue note
-Creates a new note to a single project issue.
+Creates a new note to a single project issue. If you create a note where the body
+only contains an Award Emoji, you'll receive this object back.
```
POST /projects/:id/issues/:issue_id/notes
@@ -204,6 +205,7 @@ Parameters:
### Create new snippet note
Creates a new note for a single snippet. Snippet notes are comments users can post to a snippet.
+If you create a note where the body only contains an Award Emoji, you'll receive this object back.
```
POST /projects/:id/snippets/:snippet_id/notes
@@ -332,6 +334,8 @@ Parameters:
### Create new merge request note
Creates a new note for a single merge request.
+If you create a note where the body only contains an Award Emoji, you'll receive
+this object back.
```
POST /projects/:id/merge_requests/:merge_request_id/notes
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
new file mode 100644
index 00000000000..ff6c9e4931c
--- /dev/null
+++ b/doc/api/notification_settings.md
@@ -0,0 +1,169 @@
+# Notification settings
+
+>**Note:** This feature was [introduced][ce-5632] in GitLab 8.12.
+
+**Valid notification levels**
+
+The notification levels are defined in the `NotificationSetting::level` model enumeration. Currently, these levels are recognized:
+
+```
+disabled
+participating
+watch
+global
+mention
+custom
+```
+
+If the `custom` level is used, specific email events can be controlled. Notification email events are defined in the `NotificationSetting::EMAIL_EVENTS` model variable. Currently, these events are recognized:
+
+```
+new_note
+new_issue
+reopen_issue
+close_issue
+reassign_issue
+new_merge_request
+reopen_merge_request
+close_merge_request
+reassign_merge_request
+merge_merge_request
+```
+
+## Global notification settings
+
+Get current notification settings and email address.
+
+```
+GET /notification_settings
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/notification_settings
+```
+
+Example response:
+
+```json
+{
+ "level": "participating",
+ "notification_email": "admin@example.com"
+}
+```
+
+## Update global notification settings
+
+Update current notification settings and email address.
+
+```
+PUT /notification_settings
+```
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/notification_settings?level=watch
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `level` | string | no | The global notification level |
+| `notification_email` | string | no | The email address to send notifications |
+| `new_note` | boolean | no | Enable/disable this notification |
+| `new_issue` | boolean | no | Enable/disable this notification |
+| `reopen_issue` | boolean | no | Enable/disable this notification |
+| `close_issue` | boolean | no | Enable/disable this notification |
+| `reassign_issue` | boolean | no | Enable/disable this notification |
+| `new_merge_request` | boolean | no | Enable/disable this notification |
+| `reopen_merge_request` | boolean | no | Enable/disable this notification |
+| `close_merge_request` | boolean | no | Enable/disable this notification |
+| `reassign_merge_request` | boolean | no | Enable/disable this notification |
+| `merge_merge_request` | boolean | no | Enable/disable this notification |
+
+Example response:
+
+```json
+{
+ "level": "watch",
+ "notification_email": "admin@example.com"
+}
+```
+
+## Group / project level notification settings
+
+Get current group or project notification settings.
+
+```
+GET /groups/:id/notification_settings
+GET /projects/:id/notification_settings
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/5/notification_settings
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/notification_settings
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+
+Example response:
+
+```json
+{
+ "level": "global"
+}
+```
+
+## Update group/project level notification settings
+
+Update current group/project notification settings.
+
+```
+PUT /groups/:id/notification_settings
+PUT /projects/:id/notification_settings
+```
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/5/notification_settings?level=watch
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/notification_settings?level=custom&new_note=true
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+| `level` | string | no | The global notification level |
+| `new_note` | boolean | no | Enable/disable this notification |
+| `new_issue` | boolean | no | Enable/disable this notification |
+| `reopen_issue` | boolean | no | Enable/disable this notification |
+| `close_issue` | boolean | no | Enable/disable this notification |
+| `reassign_issue` | boolean | no | Enable/disable this notification |
+| `new_merge_request` | boolean | no | Enable/disable this notification |
+| `reopen_merge_request` | boolean | no | Enable/disable this notification |
+| `close_merge_request` | boolean | no | Enable/disable this notification |
+| `reassign_merge_request` | boolean | no | Enable/disable this notification |
+| `merge_merge_request` | boolean | no | Enable/disable this notification |
+
+Example responses:
+
+```json
+{
+ "level": "watch"
+}
+
+{
+ "level": "custom",
+ "events": {
+ "new_note": true,
+ "new_issue": false,
+ "reopen_issue": false,
+ "close_issue": false,
+ "reassign_issue": false,
+ "new_merge_request": false,
+ "reopen_merge_request": false,
+ "close_merge_request": false,
+ "reassign_merge_request": false,
+ "merge_merge_request": false
+ }
+}
+```
+
+[ce-5632]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5632
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index 0b0fc39ec7e..5ef5e3f5744 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -1,10 +1,10 @@
-# GitLab as an OAuth2 client
+# GitLab as an OAuth2 provider
This document covers using the OAuth2 protocol to access GitLab.
If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md).
-OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party.
+OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party.
This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper)
@@ -22,7 +22,7 @@ In the following sections you will be introduced to the three steps needed for t
### 1. Registering the client
First, you should create an application (`/profile/applications`) in your user's account.
-Each application gets a unique App ID and App Secret parameters.
+Each application gets a unique App ID and App Secret parameters.
>**Note:**
**You should not share/leak your App ID or App Secret.**
@@ -46,10 +46,10 @@ http://myapp.com/oauth/redirect?code=1234567890&state=your_unique_state_hash
You should then use the `code` to request an access token.
>**Important:**
-It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and
-validate that value is returned and matches in the redirect request.
-This is important to prevent [CSFR attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow),
-`state` really should have been a requirement in the standard!
+It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and
+validate that value is returned and matches in the redirect request.
+This is important to prevent [CSRF attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow),
+`state` really should have been a requirement in the standard!
### 3. Requesting the access token
@@ -62,7 +62,7 @@ RestClient.post 'http://localhost:3000/oauth/token', parameters
# The response will be
{
"access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54",
- "token_type": "bearer",
+ "token_type": "bearer",
"expires_in": 7200,
"refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1"
}
@@ -95,7 +95,7 @@ curl --header "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/
---
-In this flow, a token is requested in exchange for the resource owner credentials (username and password).
+In this flow, a token is requested in exchange for the resource owner credentials (username and password).
The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the
client is part of the device operating system or a highly privileged application), and when other authorization grant types are not
available (such as an authorization code).
@@ -112,7 +112,7 @@ You can do POST request to `/oauth/token` with parameters:
{
"grant_type" : "password",
"username" : "user@example.com",
- "password" : "sekret"
+ "password" : "secret"
}
```
@@ -130,8 +130,8 @@ For testing you can use the oauth2 ruby gem:
```
client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http://example.com")
-access_token = client.password.get_token('user@example.com', 'sekret')
+access_token = client.password.get_token('user@example.com', 'secret')
puts access_token.token
```
-[personal access tokens]: ./README.md#personal-access-tokens
+[personal access tokens]: ./README.md#personal-access-tokens \ No newline at end of file
diff --git a/doc/api/projects.md b/doc/api/projects.md
index a62aaee14d7..869907b0dd7 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -85,7 +85,8 @@ Parameters:
"runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_builds": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
},
{
"id": 6,
@@ -146,7 +147,8 @@ Parameters:
"runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_builds": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
}
]
```
@@ -283,7 +285,8 @@ Parameters:
"group_access_level": 10
}
],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
}
```
@@ -453,6 +456,7 @@ Parameters:
- `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional)
- `lfs_enabled` (optional)
+- `request_access_enabled` (optional) - Allow users to request member access.
### Create project for user
@@ -480,6 +484,7 @@ Parameters:
- `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional)
- `lfs_enabled` (optional)
+- `request_access_enabled` (optional) - Allow users to request member access.
### Edit project
@@ -508,13 +513,14 @@ Parameters:
- `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional)
- `lfs_enabled` (optional)
+- `request_access_enabled` (optional) - Allow users to request member access.
On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned.
### Fork project
-Forks a project into the user namespace of the authenticated user.
+Forks a project into the user namespace of the authenticated user or the one provided.
```
POST /projects/fork/:id
@@ -523,6 +529,7 @@ POST /projects/fork/:id
Parameters:
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
+- `namespace` (optional) - The ID or path of the namespace that the project will be forked to
### Star a project
@@ -587,7 +594,8 @@ Example response:
"star_count": 1,
"public_builds": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
}
```
@@ -654,7 +662,8 @@ Example response:
"star_count": 0,
"public_builds": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
}
```
@@ -741,7 +750,8 @@ Example response:
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
}
```
@@ -828,7 +838,8 @@ Example response:
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false
+ "only_allow_merge_if_build_succeeds": false,
+ "request_access_enabled": false
}
```
@@ -888,6 +899,7 @@ Parameters:
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
- `group_id` (required) - The ID of a group
- `group_access` (required) - Level of permissions for sharing
+- `expires_at` - Share expiration date in ISO 8601 format: 2016-09-26
## Hooks
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index fc3af5544de..1bc6a24e914 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -44,7 +44,7 @@ POST /projects/:id/repository/files
```
```bash
-curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20content&commit_message=create%20a%20new%20file'
+curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
```
Example response:
@@ -61,6 +61,8 @@ Parameters:
- `file_path` (required) - Full path to new file. Ex. lib/class.rb
- `branch_name` (required) - The name of branch
- `encoding` (optional) - 'text' or 'base64'. Text is default.
+- `author_email` (optional) - Specify the commit author's email address
+- `author_name` (optional) - Specify the commit author's name
- `content` (required) - File content
- `commit_message` (required) - Commit message
@@ -71,7 +73,7 @@ PUT /projects/:id/repository/files
```
```bash
-curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20other%20content&commit_message=update%20file'
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
```
Example response:
@@ -88,6 +90,8 @@ Parameters:
- `file_path` (required) - Full path to file. Ex. lib/class.rb
- `branch_name` (required) - The name of branch
- `encoding` (optional) - 'text' or 'base64'. Text is default.
+- `author_email` (optional) - Specify the commit author's email address
+- `author_name` (optional) - Specify the commit author's name
- `content` (required) - New file content
- `commit_message` (required) - Commit message
@@ -107,7 +111,7 @@ DELETE /projects/:id/repository/files
```
```bash
-curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&commit_message=delete%20file'
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
```
Example response:
@@ -123,4 +127,6 @@ Parameters:
- `file_path` (required) - Full path to file. Ex. lib/class.rb
- `branch_name` (required) - The name of branch
+- `author_email` (optional) - Specify the commit author's email address
+- `author_name` (optional) - Specify the commit author's name
- `commit_message` (required) - Commit message
diff --git a/doc/api/settings.md b/doc/api/settings.md
index a76dad0ebd4..f7ad3b4cc8e 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -41,7 +41,9 @@ Example response:
"gravatar_enabled" : true,
"sign_in_text" : null,
"container_registry_token_expire_delay": 5,
- "repository_storage": "default"
+ "repository_storage": "default",
+ "koding_enabled": false,
+ "koding_url": null
}
```
@@ -67,12 +69,14 @@ PUT /application/settings
| `default_snippet_visibility` | integer | no | What visibility level new snippets receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.|
| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. |
| `domain_blacklist_enabled` | boolean | no | Enable/disable the `domain_blacklist` |
-| `domain_blacklist` | array of strings | yes (if `domain_whitelist_enabled` is `true` | People trying to sign-up with emails from this domain will not be allowed to do so. |
+| `domain_blacklist` | array of strings | yes (if `domain_blacklist_enabled` is `true`) | People trying to sign-up with emails from this domain will not be allowed to do so. |
| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider |
| `after_sign_out_path` | string | no | Where to redirect users after logout |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
| `repository_storage` | string | no | Storage path for new projects. The value should be the name of one of the repository storage paths defined in your gitlab.yml |
-| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols.
+| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
+| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. |
+| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
@@ -103,6 +107,8 @@ Example response:
"user_oauth_applications": true,
"after_sign_out_path": "",
"container_registry_token_expire_delay": 5,
- "repository_storage": "default"
+ "repository_storage": "default",
+ "koding_enabled": false,
+ "koding_url": null
}
```
diff --git a/doc/api/users.md b/doc/api/users.md
index 54f7a2a2ace..9be4f2e6ec3 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -57,6 +57,7 @@ GET /users
"linkedin": "",
"twitter": "",
"website_url": "",
+ "organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
@@ -89,6 +90,7 @@ GET /users
"linkedin": "",
"twitter": "",
"website_url": "",
+ "organization": "",
"last_sign_in_at": null,
"confirmed_at": "2012-05-30T16:53:06.148Z",
"theme_id": 1,
@@ -147,7 +149,8 @@ Parameters:
"skype": "",
"linkedin": "",
"twitter": "",
- "website_url": ""
+ "website_url": "",
+ "organization": ""
}
```
@@ -178,6 +181,7 @@ Parameters:
"linkedin": "",
"twitter": "",
"website_url": "",
+ "organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
@@ -214,6 +218,7 @@ Parameters:
- `linkedin` (optional) - LinkedIn
- `twitter` (optional) - Twitter account
- `website_url` (optional) - Website URL
+- `organization` (optional) - Organization name
- `projects_limit` (optional) - Number of projects user can create
- `extern_uid` (optional) - External UID
- `provider` (optional) - External provider name
@@ -242,6 +247,7 @@ Parameters:
- `linkedin` - LinkedIn
- `twitter` - Twitter account
- `website_url` - Website URL
+- `organization` - Organization name
- `projects_limit` - Limit projects each user can create
- `extern_uid` - External UID
- `provider` - External provider name
@@ -296,6 +302,7 @@ GET /user
"linkedin": "",
"twitter": "",
"website_url": "",
+ "organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 10ce4ac8940..341bc85a16a 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -16,5 +16,7 @@
- [Trigger builds through the API](triggers/README.md)
- [Build artifacts](../user/project/builds/artifacts.md)
- [User permissions](../user/permissions.md#gitlab-ci)
+- [Build permissions](../user/permissions.md#build-permissions)
- [API](../api/ci/README.md)
- [CI services (linked docker containers)](services/README.md)
+- [**New CI build permissions model**](../user/project/new_ci_build_permissions_model.md) Read about what changed in GitLab 8.12 and how that affects your builds. There's a new way to access your Git submodules and LFS objects in builds.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index c134106bfd0..40f0165deef 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,17 +1,19 @@
# CI Examples
+A collection of `.gitlab-ci.yml` files is maintained at the [GitLab CI Yml project][gitlab-ci-templates].
+If your favorite programming language or framework are missing we would love your help by sending a merge request
+with a `.gitlab-ci.yml`.
+
+Apart from those, here is an collection of tutorials and guides on setting up your CI pipeline:
+
- [Testing a PHP application](php.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)
- [Test a Scala application](test-scala-application.md)
- [Using `dpl` as deployment tool](deployment/README.md)
-- Help your favorite programming language and GitLab by sending a merge request
- with a guide for that language.
-
-## Outside the documentation
-
- [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
-- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples)
+- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-- [A collection of useful .gitlab-ci.yml templates](https://gitlab.com/gitlab-org/gitlab-ci-yml)
+
+[gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index c835ebc2d44..c40cdd55ea5 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -105,7 +105,8 @@ What is important is that each job is run independently from each other.
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.
+a "CI Lint" button to go to this page under **Pipelines > Pipelines** and
+**Pipelines > Builds** in your project.
For more information and a complete `.gitlab-ci.yml` syntax, please read
[the documentation on .gitlab-ci.yml](../yaml/README.md).
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index 7c0fb225dac..b858029d25e 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -30,7 +30,8 @@ This is the universal solution which works with any type of executor
## SSH keys when using the Docker executor
You will first need to create an SSH key pair. For more information, follow the
-instructions to [generate an SSH key](../../ssh/README.md).
+instructions to [generate an SSH key](../../ssh/README.md). Do not add a comment
+to the SSH key, or the `before_script` will prompt for a passphrase.
Then, create a new **Secret Variable** in your project settings on GitLab
following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY`
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 6c6767fea0b..84048f1d25f 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -2,6 +2,10 @@
> [Introduced][ci-229] in GitLab CE 7.14.
+> **Note**:
+GitLab 8.12 has a completely redesigned build permissions system.
+Read all about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#build-triggers).
+
Triggers can be used to force a rebuild of a specific branch, tag or commit,
with an API call.
diff --git a/doc/ci/triggers/img/builds_page.png b/doc/ci/triggers/img/builds_page.png
index 2dee8ee6107..c2cf4b1852c 100644
--- a/doc/ci/triggers/img/builds_page.png
+++ b/doc/ci/triggers/img/builds_page.png
Binary files differ
diff --git a/doc/ci/triggers/img/trigger_single_build.png b/doc/ci/triggers/img/trigger_single_build.png
index baf3fc183d8..fa86f0fee3d 100644
--- a/doc/ci/triggers/img/trigger_single_build.png
+++ b/doc/ci/triggers/img/trigger_single_build.png
Binary files differ
diff --git a/doc/ci/triggers/img/trigger_variables.png b/doc/ci/triggers/img/trigger_variables.png
index 908355c33a5..b2fcc65d304 100644
--- a/doc/ci/triggers/img/trigger_variables.png
+++ b/doc/ci/triggers/img/trigger_variables.png
Binary files differ
diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png
index 69cec5cdebf..438f285ae2d 100644
--- a/doc/ci/triggers/img/triggers_page.png
+++ b/doc/ci/triggers/img/triggers_page.png
Binary files differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 4a7c21f811d..22d67bd9964 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -34,6 +34,7 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
| **CI_BUILD_REF_NAME** | all | all | The branch or tag name for which project is built |
| **CI_BUILD_REPO** | all | all | The URL to clone the Git repository |
| **CI_BUILD_TRIGGERED** | all | 0.5 | The flag to indicate that build was [triggered] |
+| **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that build was manually started |
| **CI_BUILD_TOKEN** | all | 1.2 | Token used for authenticating with the GitLab Container Registry |
| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
@@ -43,10 +44,12 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the build is run |
| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
-| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returnes the address of the registry tied to the specific project |
+| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
+| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the build |
+| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the build |
**Some of the variables are only available when using runner with at least defined version.**
@@ -60,6 +63,7 @@ export CI_BUILD_REPO="https://gitab-ci-token:abcde-1234ABCD5678ef@gitlab.com/git
export CI_BUILD_TAG="1.0.0"
export CI_BUILD_NAME="spec:other"
export CI_BUILD_STAGE="test"
+export CI_BUILD_MANUAL="true"
export CI_BUILD_TRIGGERED="true"
export CI_BUILD_TOKEN="abcde-1234ABCD5678ef"
export CI_PIPELINE_ID="1000"
@@ -76,8 +80,10 @@ export CI_RUNNER_DESCRIPTION="my runner"
export CI_RUNNER_TAGS="docker, linux"
export CI_SERVER="yes"
export CI_SERVER_NAME="GitLab"
-export CI_SERVER_REVISION="8.9.0"
-export CI_SERVER_VERSION="70606bf"
+export CI_SERVER_REVISION="70606bf"
+export CI_SERVER_VERSION="8.9.0"
+export GITLAB_USER_ID="42"
+export GITLAB_USER_EMAIL="alexzander@sporer.com"
```
### YAML-defined variables
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 58d5306f12a..16868554c1f 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -6,50 +6,6 @@ GitLab Runner to manage your project's builds.
If you want a quick introduction to GitLab CI, follow our
[quick start guide](../quick_start/README.md).
----
-
-<!-- START doctoc generated TOC please keep comment here to allow auto update -->
-<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
-**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
-
-- [.gitlab-ci.yml](#gitlab-ci-yml)
- - [image and services](#image-and-services)
- - [before_script](#before_script)
- - [after_script](#after_script)
- - [stages](#stages)
- - [types](#types)
- - [variables](#variables)
- - [cache](#cache)
- - [cache:key](#cache-key)
-- [Jobs](#jobs)
- - [script](#script)
- - [stage](#stage)
- - [only and except](#only-and-except)
- - [job variables](#job-variables)
- - [tags](#tags)
- - [allow_failure](#allow_failure)
- - [when](#when)
- - [Manual actions](#manual-actions)
- - [environment](#environment)
- - [artifacts](#artifacts)
- - [artifacts:name](#artifacts-name)
- - [artifacts:when](#artifacts-when)
- - [artifacts:expire_in](#artifacts-expire_in)
- - [dependencies](#dependencies)
- - [before_script and after_script](#before_script-and-after_script)
-- [Git Strategy](#git-strategy)
-- [Shallow cloning](#shallow-cloning)
-- [Hidden keys](#hidden-keys)
-- [Special YAML features](#special-yaml-features)
- - [Anchors](#anchors)
-- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml)
-- [Skipping builds](#skipping-builds)
-- [Examples](#examples)
-
-<!-- END doctoc generated TOC please keep comment here to allow auto update -->
-
----
-
## .gitlab-ci.yml
From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML)
@@ -134,8 +90,7 @@ builds, including deploy builds. This can be an array or a multi-line string.
### after_script
->**Note:**
-Introduced in GitLab 8.7 and requires Gitlab Runner v1.2
+> Introduced in GitLab 8.7 and requires Gitlab Runner v1.2
`after_script` is used to define the command that will be run after for all
builds. This has to be an array or a multi-line string.
@@ -179,11 +134,10 @@ Alias for [stages](#stages).
### variables
->**Note:**
-Introduced in GitLab Runner v0.5.0.
+> Introduced in GitLab Runner v0.5.0.
GitLab CI allows you to add variables to `.gitlab-ci.yml` that are set in the
-build environment. The variables are stored in the git repository and are meant
+build environment. The variables are stored in the Git repository and are meant
to store non-sensitive project configuration, for example:
```yaml
@@ -198,10 +152,11 @@ thus allowing to fine tune them.
Variables can be also defined on [job level](#job-variables).
+[Learn more about variables.](../variables/README.md)
+
### cache
->**Note:**
-Introduced in GitLab Runner v0.7.0.
+> Introduced in GitLab Runner v0.7.0.
`cache` is used to specify a list of files and directories which should be
cached between builds.
@@ -262,8 +217,7 @@ will be always present. For implementation details, please check GitLab Runner.
#### cache:key
->**Note:**
-Introduced in GitLab Runner v1.0.0.
+> Introduced in GitLab Runner v1.0.0.
The `key` directive allows you to define the affinity of caching
between jobs, allowing to have a single cache for all jobs,
@@ -573,8 +527,7 @@ The above script will:
#### Manual actions
->**Note:**
-Introduced in GitLab 8.10.
+> Introduced in GitLab 8.10.
Manual actions are a special type of job that are not executed automatically;
they need to be explicitly started by a user. Manual actions can be started
@@ -585,17 +538,16 @@ An example usage of manual actions is deployment to production.
### environment
->**Note:**
-Introduced in GitLab 8.9.
+> Introduced in GitLab 8.9.
-`environment` is used to define that a job deploys to a specific environment.
+`environment` is used to define that a job deploys to a specific [environment].
This allows easy tracking of all deployments to your environments straight from
GitLab.
If `environment` is specified and no environment under that name exists, a new
one will be created automatically.
-The `environment` name must contain only letters, digits, '-' and '_'. Common
+The `environment` name must contain only letters, digits, '-', '_', '/', '$', '{', '}' and spaces. Common
names are `qa`, `staging`, and `production`, but you can use whatever name works
with your workflow.
@@ -613,6 +565,35 @@ deploy to production:
The `deploy to production` job will be marked as doing deployment to
`production` environment.
+#### dynamic environments
+
+> [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
+
+`environment` can also represent a configuration hash with `name` and `url`.
+These parameters can use any of the defined CI [variables](#variables)
+(including predefined, secure variables and `.gitlab-ci.yml` variables).
+
+The common use case is to create dynamic environments for branches and use them
+as review apps.
+
+---
+
+**Example configurations**
+
+```
+deploy as review app:
+ stage: deploy
+ script: ...
+ environment:
+ name: review-apps/$CI_BUILD_REF_NAME
+ url: https://$CI_BUILD_REF_NAME.review.example.com/
+```
+
+The `deploy as review app` job will be marked as deployment to dynamically
+create the `review-apps/branch-name` environment.
+
+This environment should be accessible under `https://branch-name.review.example.com/`.
+
### artifacts
>**Notes:**
@@ -680,8 +661,7 @@ be available for download in the GitLab UI.
#### artifacts:name
->**Note:**
-Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
The `name` directive allows you to define the name of the created artifacts
archive. That way, you can have a unique name for every archive which could be
@@ -744,8 +724,7 @@ job:
#### artifacts:when
->**Note:**
-Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
+> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
`artifacts:when` is used to upload artifacts on build failure or despite the
failure.
@@ -770,8 +749,7 @@ job:
#### artifacts:expire_in
->**Note:**
-Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
+> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
`artifacts:expire_in` is used to delete uploaded artifacts after the specified
time. By default, artifacts are stored on GitLab forever. `expire_in` allows you
@@ -806,8 +784,7 @@ job:
### dependencies
->**Note:**
-Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
This feature should be used in conjunction with [`artifacts`](#artifacts) and
allows you to define the artifacts to pass between different builds.
@@ -881,9 +858,8 @@ job:
## Git Strategy
->**Note:**
-Introduced in GitLab 8.9 as an experimental feature. May change in future
-releases or be removed completely.
+> Introduced in GitLab 8.9 as an experimental feature. May change in future
+ releases or be removed completely.
You can set the `GIT_STRATEGY` used for getting recent application code. `clone`
is slower, but makes sure you have a clean directory before every build. `fetch`
@@ -905,8 +881,7 @@ variables:
## Shallow cloning
->**Note:**
-Introduced in GitLab 8.9 as an experimental feature. May change in future
+> Introduced in GitLab 8.9 as an experimental feature. May change in future
releases or be removed completely.
You can specify the depth of fetching and cloning using `GIT_DEPTH`. This allows
@@ -936,8 +911,7 @@ variables:
## Hidden keys
->**Note:**
-Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
Keys that start with a dot (`.`) will be not processed by GitLab CI. You can
use this feature to ignore jobs, or use the
@@ -965,8 +939,7 @@ Read more about the various [YAML features](https://learnxinyminutes.com/docs/ya
### Anchors
->**Note:**
-Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
YAML also has a handy feature called 'anchors', which let you easily duplicate
content across your document. Anchors can be used to duplicate/inherit
@@ -1086,6 +1059,13 @@ test:mysql:
You can see that the hidden keys are conveniently used as templates.
+## Triggers
+
+Triggers can be used to force a rebuild of a specific branch, tag or commit,
+with an API call.
+
+[Read more in the triggers documentation.](../triggers/README.md)
+
## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint.
@@ -1102,3 +1082,5 @@ Visit the [examples README][examples] to see a list of examples using GitLab
CI with various languages.
[examples]: ../examples/README.md
+[ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323
+[environment]: ../environments.md
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index 047a0b08406..d7740647a91 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -78,9 +78,9 @@ delete them.
> **Note:**
This feature requires GitLab 8.8 and GitLab Runner 1.2.
-Make sure that your GitLab Runner is configured to allow building docker images.
-You have to check the [Using Docker Build documentation](../ci/docker/using_docker_build.md).
-Then see the CI documentation on [Using the GitLab Container Registry](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
+Make sure that your GitLab Runner is configured to allow building Docker images by
+following the [Using Docker Build](../ci/docker/using_docker_build.md)
+and [Using the GitLab Container Registry documentation](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
## Limitations
diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md
index 4620bb2dcde..31164ccd465 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -1,39 +1,4 @@
-# Issue closing pattern
+This document was split into:
-When a commit or merge request resolves one or more issues, it is possible to automatically have these issues closed when the commit or merge request lands in the project's default branch.
-
-If a commit message or merge request description contains a sentence matching the regular expression below, all issues referenced from
-the matched text will be closed. This happens when the commit is pushed to a project's default branch, or when a commit or merge request is merged into there.
-
-When not specified, the default `issue_closing_pattern` as shown below will be used:
-
-```bash
-((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
-```
-
-Here, `%{issue_ref}` is a complex regular expression defined inside GitLab, that matches a reference to a local issue (`#123`), cross-project issue (`group/project#123`) or a link to an issue (`https://gitlab.example.com/group/project/issues/123`).
-
-For example:
-
-```
-git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes group/otherproject#22). This commit is also related to #17 and fixes #18, #19 and https://gitlab.example.com/group/otherproject/issues/23."
-```
-
-will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it does not match the pattern. It also works with multiline commit messages.
-
-Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site
-to test your own patterns.
-Because Rubular doesn't understand `%{issue_ref}`, you can replace this by `#\d+` in testing, which matches only local issue references like `#123`.
-
-## Change the pattern
-
-For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`:
-
-```
-issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)'
-```
-
-For manual installs you can customize the pattern in [gitlab.yml][0] using the `issue_closing_pattern` key.
-
-[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example
-[1]: http://rubular.com/r/Xmbexed1OJ
+- [administration/issue_closing_pattern.md](../administration/issue_closing_pattern.md).
+- [user/project/issues/automatic_issue_closing](../user/project/issues/automatic_issue_closing.md).
diff --git a/doc/development/README.md b/doc/development/README.md
index 58c00f618fa..9706cb1de7f 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -13,6 +13,7 @@
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
- [Testing standards and style guidelines](testing.md)
- [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements
+- [Frontend guidelines](frontend.md)
- [SQL guidelines](sql.md) for SQL guidelines
## Process
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
new file mode 100644
index 00000000000..f879cd57e25
--- /dev/null
+++ b/doc/development/frontend.md
@@ -0,0 +1,225 @@
+# Frontend Development Guidelines
+
+This document describes various guidelines to ensure consistency and quality
+across GitLab's frontend team.
+
+## Overview
+
+GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] with
+[Hamlit][hamlit]. Be wary of [the limitations that come with using
+Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
+[ES6 by way of Babel][es6].
+
+The asset pipeline is [Sprockets][sprockets], which handles the concatenation,
+minification, and compression of our assets.
+
+[jQuery][jquery] is used throughout the application's JavaScript, with
+[Vue.js][vue] for particularly advanced, dynamic elements.
+
+### Vue
+
+For more complex frontend features, we recommend using Vue.js. It shares
+some ideas with React.js as well as Angular.
+
+To get started with Vue, read through [their documentation][vue-docs].
+
+## Performance
+
+### Resources
+
+- [WebPage Test][web-page-test] for testing site loading time and size.
+- [Google PageSpeed Insights][pagespeed-insights] grades web pages and provides feedback to improve the page.
+- [Profiling with Chrome DevTools][google-devtools-profiling]
+- [Browser Diet][browser-diet] is a community-built guide that catalogues practical tips for improving web page performance.
+
+### Page-specific JavaScript
+
+Certain pages may require the use of a third party library, such as [d3][d3] for
+the User Activity Calendar and [Chart.js][chartjs] for the Graphs pages. These
+libraries increase the page size significantly, and impact load times due to
+bandwidth bottlenecks and the browser needing to parse more JavaScript.
+
+In cases where libraries are only used on a few specific pages, we use
+"page-specific JavaScript" to prevent the main `application.js` file from
+becoming unnecessarily large.
+
+Steps to split page-specific JavaScript from the main `application.js`:
+
+1. Create a directory for the specific page(s), e.g. `graphs/`.
+1. In that directory, create a `namespace_bundle.js` file, e.g. `graphs_bundle.js`.
+1. In `graphs_bundle.js` add the line `//= require_tree .`, this adds all other files in the directory to the bundle.
+1. Add any necessary libraries to `app/assets/javascripts/lib/`, all files directly descendant from this directory will be precompiled as separate assets, in this case `chart.js` would be added.
+1. Add the new "bundle" file to the list of precompiled assets in
+`config/application.rb`.
+ - For example: `config.assets.precompile << "graphs/graphs_bundle.js"`.
+1. Move code reliant on these libraries into the `graphs` directory.
+1. In the relevant views, add the scripts to the page with the following:
+
+```haml
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/chart.js')
+ = page_specific_javascript_tag('graphs/graphs_bundle.js')
+```
+
+The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js`
+is separated from the bundle file so it can be cached separately from the bundle
+and reused for other pages that also rely on the library. For an example, see
+[this Haml file][page-specific-js-example].
+
+### Minimizing page size
+
+A smaller page size means the page loads faster (especially important on mobile
+and poor connections), the page is parsed more quickly by the browser, and less
+data is used for users with capped data plans.
+
+General tips:
+
+- Don't add new fonts.
+- Prefer font formats with better compression, e.g. WOFF2 is better than WOFF, which is better than TTF.
+- Compress and minify assets wherever possible (For CSS/JS, Sprockets does this for us).
+- If some functionality can reasonably be achieved without adding extra libraries, avoid them.
+- Use page-specific JavaScript as described above to dynamically load libraries that are only needed on certain pages.
+
+## Accessibility
+
+### Resources
+
+[Chrome Accessibility Developer Tools][chrome-accessibility-developer-tools]
+are useful for testing for potential accessibility problems in GitLab.
+
+Accessibility best-practices and more in-depth information is available on
+[the Audit Rules page][audit-rules] for the Chrome Accessibility Developer Tools.
+
+## Security
+
+### Resources
+
+[Mozilla’s HTTP Observatory CLI][observatory-cli] and the
+[Qualys SSL Labs Server Test][qualys-ssl] are good resources for finding
+potential problems and ensuring compliance with security best practices.
+
+<!-- Uncomment these sections when CSP/SRI are implemented.
+### Content Security Policy (CSP)
+
+Content Security Policy is a web standard that intends to mitigate certain
+forms of Cross-Site Scripting (XSS) as well as data injection.
+
+Content Security Policy rules should be taken into consideration when
+implementing new features, especially those that may rely on connection with
+external services.
+
+GitLab's CSP is used for the following:
+
+- Blocking plugins like Flash and Silverlight from running at all on our pages.
+- Blocking the use of scripts and stylesheets downloaded from external sources.
+- Upgrading `http` requests to `https` when possible.
+- Preventing `iframe` elements from loading in most contexts.
+
+Some exceptions include:
+
+- Scripts from Google Analytics and Piwik if either is enabled.
+- Connecting with GitHub, Bitbucket, GitLab.com, etc. to allow project importing.
+- Connecting with Google, Twitter, GitHub, etc. to allow OAuth authentication.
+
+We use [the Secure Headers gem][secure_headers] to enable Content
+Security Policy headers in the GitLab Rails app.
+
+Some resources on implementing Content Security Policy:
+
+- [MDN Article on CSP][mdn-csp]
+- [GitHub’s CSP Journey on the GitHub Engineering Blog][github-eng-csp]
+- The Dropbox Engineering Blog's series on CSP: [1][dropbox-csp-1], [2][dropbox-csp-2], [3][dropbox-csp-3], [4][dropbox-csp-4]
+
+### Subresource Integrity (SRI)
+
+Subresource Integrity prevents malicious assets from being provided by a CDN by
+guaranteeing that the asset downloaded is identical to the asset the server
+is expecting.
+
+The Rails app generates a unique hash of the asset, which is used as the
+asset's `integrity` attribute. The browser generates the hash of the asset
+on-load and will reject the asset if the hashes do not match.
+
+All CSS and JavaScript assets should use Subresource Integrity. For implementation details,
+see the documentation for [the Sprockets implementation of SRI][sprockets-sri].
+
+Some resources on implementing Subresource Integrity:
+
+- [MDN Article on SRI][mdn-sri]
+- [Subresource Integrity on the GitHub Engineering Blog][github-eng-sri]
+
+-->
+
+### Including external resources
+
+External fonts, CSS, and JavaScript should never be used with the exception of
+Google Analytics and Piwik - and only when the instance has enabled it. Assets
+should always be hosted and served locally from the GitLab instance. Embedded
+resources via `iframes` should never be used except in certain circumstances
+such as with ReCaptcha, which cannot be used without an `iframe`.
+
+### Avoiding inline scripts and styles
+
+In order to protect users from [XSS vulnerabilities][xss], we will disable inline scripts in the future using Content Security Policy.
+
+While inline scripts can be useful, they're also a security concern. If
+user-supplied content is unintentionally left un-sanitized, malicious users can
+inject scripts into the web app.
+
+Inline styles should be avoided in almost all cases, they should only be used
+when no alternatives can be found. This allows reusability of styles as well as
+readability.
+
+## Style guides and linting
+
+See the relevant style guides for our guidelines and for information on linting:
+
+- [SCSS][scss-style-guide]
+
+## Testing
+
+Feature tests need to be written for all new features. Regression tests
+also need to be written for all bug fixes to prevent them from occurring
+again in the future.
+
+See [the Testing Standards and Style Guidelines](testing.md) for more
+information.
+
+## Supported browsers
+
+For our currently-supported browsers, see our [requirements][requirements].
+
+[rails]: http://rubyonrails.org/
+[haml]: http://haml.info/
+[hamlit]: https://github.com/k0kubun/hamlit
+[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
+[scss]: http://sass-lang.com/
+[es6]: https://babeljs.io/
+[sprockets]: https://github.com/rails/sprockets
+[jquery]: https://jquery.com/
+[vue]: http://vuejs.org/
+[vue-docs]: http://vuejs.org/guide/index.html
+[web-page-test]: http://www.webpagetest.org/
+[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
+[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
+[browser-diet]: https://browserdiet.com/
+[d3]: https://d3js.org/
+[chartjs]: http://www.chartjs.org/
+[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
+[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
+[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
+[observatory-cli]: https://github.com/mozilla/http-observatory-cli)
+[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
+[secure_headers]: https://github.com/twitter/secureheaders
+[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
+[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
+[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
+[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
+[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
+[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
+[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
+[github-eng-sri]: http://githubengineering.com/subresource-integrity/
+[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support
+[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
+[scss-style-guide]: scss_styleguide.md
+[requirements]: ../install/requirements.md#supported-web-browsers
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index c2272ab0a2b..105e2f1242a 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -137,3 +137,18 @@ end
```
Here the final value of `sleep_real_time` will be `3`, _not_ `1`.
+
+## Tracking Custom Events
+
+Besides instrumenting code GitLab Performance Monitoring also supports tracking
+of custom events. This is primarily intended to be used for tracking business
+metrics such as the number of Git pushes, repository imports, and so on.
+
+To track a custom event simply call `Gitlab::Metrics.add_event` passing it an
+event name and a custom set of (optional) tags. For example:
+
+```ruby
+Gitlab::Metrics.add_event(:user_login, email: current_user.email)
+```
+
+Event names should be verbs such as `push_repository` and `remove_branch`.
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index b8fab3aaff7..61b0fbc89c9 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -9,10 +9,10 @@ a big burden for most organizations. For this reason it is important that your
migrations are written carefully, can be applied online and adhere to the style guide below.
Migrations should not require GitLab installations to be taken offline unless
-_absolutely_ necessary. If a migration requires downtime this should be
-clearly mentioned during the review process as well as being documented in the
-monthly release post. For more information see the "Downtime Tagging" section
-below.
+_absolutely_ necessary - see the ["What Requires Downtime?"](what_requires_downtime.md)
+page. If a migration requires downtime, this should be clearly mentioned during
+the review process, as well as being documented in the monthly release post. For
+more information, see the "Downtime Tagging" section below.
When writing your migrations, also consider that databases might have stale data
or inconsistencies and guard for that. Try to make as little assumptions as possible
@@ -111,6 +111,28 @@ class MyMigration < ActiveRecord::Migration
end
```
+
+## Integer column type
+
+By default, an integer column can hold up to a 4-byte (32-bit) number. That is
+a max value of 2,147,483,647. Be aware of this when creating a column that will
+hold file sizes in byte units. If you are tracking file size in bytes this
+restricts the maximum file size to just over 2GB.
+
+To allow an integer column to hold up to an 8-byte (64-bit) number, explicitly
+set the limit to 8-bytes. This will allow the column to hold a value up to
+9,223,372,036,854,775,807.
+
+Rails migration example:
+
+```
+add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
+
+# or
+
+add_column(:projects, :foo, :integer, default: 10, limit: 8)
+```
+
## Testing
Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct.
diff --git a/doc/gitlab-basics/add-file.md b/doc/gitlab-basics/add-file.md
index 57136ac5c39..ff10a98e8f5 100644
--- a/doc/gitlab-basics/add-file.md
+++ b/doc/gitlab-basics/add-file.md
@@ -25,7 +25,3 @@ Add all the information that you'd like to include in your file:
Add a commit message based on what you just added and then click on "commit changes":
![Commit changes](basicsimages/commit_changes.png)
-
-### Note
-Besides its regular files, every directory needs a README.md or README.html file which works like an index, telling
-what the directory is about. It's the first document you'll find when you open a directory.
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
index 5221d85b661..da9a165b8f5 100644
--- a/doc/gitlab-basics/create-issue.md
+++ b/doc/gitlab-basics/create-issue.md
@@ -1,6 +1,6 @@
# 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.
+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.
@@ -24,4 +24,4 @@ You may assign the Issue to a user, add a milestone and add labels (they are all
![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://docs.gitlab.com/ce/customization/issue_closing.html).
+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](../user/project/issues/automatic_issue_closing.md).
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 9522c3e7170..68ed20ef5bf 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -108,7 +108,7 @@ Then select 'Internet Site' and press enter to confirm the hostname.
## 2. Ruby
-_**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future.
+**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future.
The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example,
@@ -268,9 +268,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-12-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-13-stable gitlab
-**Note:** You can change `8-12-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-13-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -381,7 +381,7 @@ sudo usermod -aG redis git
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 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 SKIP_STORAGE_VALIDATION=true
# By default, the gitlab-shell config is generated from your main GitLab config.
# You can review (and modify) the gitlab-shell config as follows:
@@ -400,7 +400,7 @@ If you are not using Linux you may have to run `gmake` instead of
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 v0.7.11
+ sudo -u git -H git checkout v0.8.2
sudo -u git -H make
### Initialize Database and Activate Advanced Features
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 571f1a38358..766a7119943 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -63,30 +63,30 @@ If you have enough RAM memory and a recent CPU the speed of GitLab is mainly lim
### Memory
-You need at least 2GB of addressable memory (RAM + swap) to install and use GitLab!
+You need at least 4GB of addressable memory (RAM + swap) to install and use GitLab!
The operating system and any other running applications will also be using memory
-so keep in mind that you need at least 2GB available before running GitLab. With
+so keep in mind that you need at least 4GB available before running GitLab. With
less memory GitLab will give strange errors during the reconfigure run and 500
errors during usage.
-- 512MB RAM + 1.5GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advice.
-- 1GB RAM + 1GB swap supports up to 100 users but it will be very slow
-- **2GB RAM** is the **recommended** memory size for all installations and supports up to 100 users
-- 4GB RAM supports up to 1,000 users
-- 8GB RAM supports up to 2,000 users
-- 16GB RAM supports up to 4,000 users
-- 32GB RAM supports up to 8,000 users
-- 64GB RAM supports up to 16,000 users
-- 128GB RAM supports up to 32,000 users
+- 1GB RAM + 3GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advice.
+- 2GB RAM + 2GB swap supports up to 100 users but it will be very slow
+- **4GB RAM** is the **recommended** memory size for all installations and supports up to 100 users
+- 8GB RAM supports up to 1,000 users
+- 16GB RAM supports up to 2,000 users
+- 32GB RAM supports up to 4,000 users
+- 64GB RAM supports up to 8,000 users
+- 128GB RAM supports up to 16,000 users
+- 256GB RAM supports up to 32,000 users
- More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/)
-We recommend having at least 1GB of swap on your server, even if you currently have
+We recommend having at least 2GB of swap on your server, even if you currently have
enough available RAM. Having swap will help reduce the chance of errors occurring
if your available memory changes.
Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those.
-## Gitlab Runner
+## GitLab Runner
We strongly advise against installing GitLab Runner on the same machine you plan
to install GitLab on. Depending on how you decide to configure GitLab Runner and
@@ -113,10 +113,8 @@ It's possible to increase the amount of unicorn workers and this will usually he
For most instances we recommend using: CPU cores + 1 = unicorn workers.
So for a machine with 2 cores, 3 unicorn workers is ideal.
-For all machines that have 1GB and up we recommend a minimum of three unicorn workers.
-If you have a 512MB machine with a magnetic (non-SSD) swap drive we recommend to configure only one Unicorn worker to prevent excessive swapping.
-With one Unicorn worker only git over ssh access will work because the git over HTTP access requires two running workers (one worker to receive the user request and one worker for the authorization check).
-If you have a 512MB machine with a SSD drive you can use two Unicorn workers, this will allow HTTP access although it will be slow due to swapping.
+For all machines that have 2GB and up we recommend a minimum of three unicorn workers.
+If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping.
To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings).
diff --git a/doc/intro/README.md b/doc/intro/README.md
index 1850031eb26..1790b2b761f 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -22,10 +22,10 @@ Create merge requests and review code.
- [Fork a project and contribute to it](../workflow/forking_workflow.md)
- [Create a new merge request](../gitlab-basics/add-merge-request.md)
-- [Automatically close issues from merge requests](../customization/issue_closing.md)
-- [Automatically merge when your builds succeed](../workflow/merge_when_build_succeeds.md)
-- [Revert any commit](../workflow/revert_changes.md)
-- [Cherry-pick any commit](../workflow/cherry_pick_changes.md)
+- [Automatically close issues from merge requests](../user/project/issues/automatic_issue_closing.md)
+- [Automatically merge when your builds succeed](../user/project/merge_requests/merge_when_build_succeeds.md)
+- [Revert any commit](../user/project/merge_requests/revert_changes.md)
+- [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md)
## Test and Deploy
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 835af5443a3..3f4056dc440 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -79,6 +79,9 @@ gitlab_rails['backup_upload_connection'] = {
'region' => 'eu-west-1',
'aws_access_key_id' => 'AKIAKIAKI',
'aws_secret_access_key' => 'secret123'
+ # If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
+ # ie. 'aws_access_key_id' => '',
+ # 'use_iam_profile' => 'true'
}
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
```
@@ -95,6 +98,9 @@ For installations from source:
region: eu-west-1
aws_access_key_id: AKIAKIAKI
aws_secret_access_key: 'secret123'
+ # If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
+ # ie. aws_access_key_id: ''
+ # use_iam_profile: 'true'
# 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
diff --git a/doc/university/README.md b/doc/university/README.md
new file mode 100644
index 00000000000..6ca1c20c9b2
--- /dev/null
+++ b/doc/university/README.md
@@ -0,0 +1,139 @@
+
+## What is GitLab University
+
+_GitLab University_ has as a goal to teach the fundamentals of **Version Control with Git and GitLab** through courses that cover topics which can be mastered in around 2 hours.
+
+_University materials don't replace our [Documentation](http://docs.gitlab.com) or [Blog Articles](https://about.gitlab.com/blog/)._
+
+---
+
+### On this page
+
++ [GITx] Git
++ [OPSx] DevOps
++ [GLBx] GitLab Basics
++ [INTx] GitLab Integrations
++ [GLFx] GitLab Workflows
++ [GLEx] GitLab Enterprise Edition extra features
++ [GCIx] GitLab CI
++ [ECO] Ecosystem
++ [COM] Competition comparison
++ [SPTx] Support Bootcamp
++ [SLSx] Sales Bootcamp
++ [TRAx] Trainings
+
+---
+
++ [GIT1] [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29)
++ [GIT2] [Operating Systems and How Git Works](https://drive.google.com/a/gitlab.com/file/d/0B41DBToSSIG_OVYxVFJDOGI3Vzg/view?usp=sharing)
++ [GIT3] [Intro to Git](https://www.codeschool.com/account/courses/try-git)
+
+---
+
++ [OPS1] [What is Omnibus](https://www.youtube.com/watch?v=XTmpKudd-Oo)
++ [OPS2] [Installing GitLab](https://www.youtube.com/watch?v=Q69YaOjqNhg)
++ [OPS3] [Configuring an external PostgreSQL database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-non-packaged-postgresql-database-management-server)
++ [OPS5] [Importing from Other Tools or SVN](http://doc.gitlab.com/ee/workflow/importing/)
++ [OPS6] [High Availability Documentation](https://about.gitlab.com/high-availability/)
++ [OPS7] [Managing LDAP, Active Directory](https://www.youtube.com/watch?v=HPMjM-14qa8)
++ [OPS8] [Scalability and High Availability](https://www.youtube.com/watch?v=cXRMJJb6sp4&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=2)
++ [OPS9] [High Availability on AWS](high-availability/aws/README.md)
+
+---
+
++ [GLB1] [Terminology](glossary/README.md)
++ [GLB2] [GitLab Basics](http://doc.gitlab.com/ce/gitlab-basics/README.html)
++ [GLB3] [Demo of GitLab.com](https://www.youtube.com/watch?v=WaiL5DGEMR4)
++ [GLB4] [Create and Add your SSH key to GitLab](https://www.youtube.com/watch?v=54mxyLo3Mqk)
++ [GLB5] [Repositories, Projects and Groups](https://www.youtube.com/watch?v=4TWfh1aKHHw&index=1&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
++ [GLB6] [Creating a Project in GitLab](https://www.youtube.com/watch?v=7p0hrpNaJ14)
++ [GLB7] [Issues and Merge Requests](https://www.youtube.com/watch?v=raXvuwet78M)
++ [GLB8] [Big files in Git (Git LFS, Annex)](https://gitlab.com/gitlab-org/University/blob/master/classes/git_lfs_and_annex.md)
+
+---
+
++ [INT1] [JIRA and Jenkins integrations in GitLab](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415)
++ [INT2] [Integrating JIRA with GitLab](http://doc.gitlab.com/ee/integration/jira.html)
++ [INT3] [Integrating Jenkins with GitLab](http://doc.gitlab.com/ee/integration/jenkins.html)
++ [INT4] [Integrating Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md)
++ [INT5] [Documentation on Integrating Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md)
+
+---
+
++ [GLF1] [GitLab Flow](https://www.youtube.com/watch?v=UGotqAUACZA)
+
+---
+
++ [GLE1] [Configuring an external MySQL database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-mysql-database-management-server-enterprise-edition-only)
++ [GLE2] [Managing Permissions within EE](https://www.youtube.com/watch?v=DjUoIrkiNuM)
++ [GLE3] [Upcoming in EE and Big files in Git (Git LFS, Annex)](https://gitlab.com/gitlab-org/University/blob/master/classes/upcoming_in_ee.md)
+
+---
+
++ [GCI1] [GitLab CI product page](https://about.gitlab.com/gitlab-ci/)
++ [GCI2] [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
+
+---
+
++ [COM1] [GitLab compared to other tools](https://about.gitlab.com/comparison/)
++ [COM2] [Compare GitLab versions](https://about.gitlab.com/features/#compare)
++ [COM3] [Innersourcing article](https://about.gitlab.com/2014/09/05/innersourcing-using-the-open-source-workflow-to-improve-collaboration-within-an-organization/)
+
+---
+
++ [ECO1] [Ecosystem Overview](https://www.youtube.com/watch?v=sXlhgPK1NTY&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=6)
++ [ECO2] [Positioning FAQ](https://about.gitlab.com/handbook/positioning-faq)
++ [ECO3] [GitLab Ecosystem slides](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit)
++ [ECO4] [Customer Use-Cases](https://about.gitlab.com/handbook/use-cases/)
+
+---
+
++ [SPT1] [Support Path](support/README.md)
++ [SPT2] [End User Training Material](https://gitlab.com/gitlab-org/University/blob/master/training/user_training.md)
++ [SPT3] [Materials for Training Sessions](https://gitlab.com/gitlab-org/University/tree/master/training/topics)
+
+---
+
++ [SLS1] [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/)
++ [SLS2] [GitLab Direction](https://about.gitlab.com/direction/)
+
+---
+
++ [TRA1] [End User Training](training/end-user/README.md)
+
+---
+
+### External Resources
+
++ [DOC] GitLab Documentation
+ + [Set up and use GitLab Pages](http://doc.gitlab.com/ee/pages/README.html)
+ + [Markdown Reference](http://doc.gitlab.com/ce/markdown/markdown.html)
+
++ [GLW] GitLab Workshop (@ Platzi)
+ + [GitLab Workshop Part 1: Basics of Git and GitLab](https://courses.platzi.com/classes/git-gitlab/)
+ + [Create a GitLab Account](https://courses.platzi.com/classes/git-gitlab/concepto/first-steps/create-an-account-on-gitlab/material/)
+
++ [GLY] GitLab YouTube Videos
+ + [Making GitLab Great for Everyone, our response to the Dear GitHub letter](https://www.youtube.com/watch?v=GGC40y4vMx0)
+ + [Compared to Atlassian (Recorded on 2016-03-03) ](https://youtu.be/Nbzp1t45ERo)
+
++ [GLI] GitLab Team-Only Access
+ + [GitLab architecture for noobs](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/development/architecture.md)
+ + [Client Assessment of GitLab versus GitHub](https://docs.google.com/a/gitlab.com/spreadsheets/d/18cRF9Y5I6I7Z_ab6qhBEW55YpEMyU4PitZYjomVHM-M/edit?usp=sharing)
+
++ [KNT] Slides & Keynotes by GitLabbers & other individuals
+ + [Why Git and GitLab slide deck](https://docs.google.com/a/gitlab.com/presentation/d/1RcZhFmn5VPvoFu6UMxhMOy7lAsToeBZRjLRn0LIdaNc/)
+ + [Git Workshop](https://docs.google.com/presentation/d/1JzTYD8ij9slejV2-TO-NzjCvlvj6mVn9BORePXNJoMI/)
+
++ Others (not created by GitLab)
+ + [Dev Ops terminology](https://xebialabs.com/glossary/)
+ + [Continuous Delivery vs Continuous Deployment](https://www.youtube.com/watch?v=igwFj8PPSnw)
+ + [Periodic Table of DevOps Tools](https://xebialabs.com/periodic-table-of-devops-tools/)
+ + [State of Dev Ops 2015 Report by Puppet Labs](https://puppetlabs.com/sites/default/files/2015-state-of-devops-report.pdf) Insightful Chapters to understand the Impact of Continuous Delivery on Performance (Chapter 4), the Application Architecture (Chapter 5) and How IT Managers can help their teams win (Chapter 6).
+ + [2011 WSJ article by Mark Andreeson - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
+ + [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/)
+ + [2015 Venture Beat article - Actually, Open Source is Eating the World](http://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/)
+ + [Customer review of GitLab with talking points on why they prefer GitLab](https://www.enovate.co.uk/web-design-blog/2015/11/25/gitlab-review/)
+ + [3rd party tool comparison](http://technologyconversations.com/2015/10/16/github-vs-gitlabs-vs-bitbucket-server-formerly-stash/)
+ + [Amazon's transition to Continuous Delivery](https://www.youtube.com/watch?v=esEFaY0FDKc)
+ + [Article on Continuous Integration from ThoughtWorks](https://www.thoughtworks.com/continuous-integration)
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
new file mode 100644
index 00000000000..a86ff165f2e
--- /dev/null
+++ b/doc/university/glossary/README.md
@@ -0,0 +1,482 @@
+
+## What is the Glossary
+
+This contains a simplified list and definitions of some of the terms that you will encounter in your day to day activities when working with GitLab.
+Please add any terms that you discover that you think would be useful for others.
+
+### 2FA
+
+User authentication by combination of 2 different steps during login. This allows for more security.
+
+### Access Levels
+
+Process of selective restriction to create, view, modify or delete a resource based on a set of assigned permissions.
+See, [GitLab's Permission Guidelines](http://doc.gitlab.com/ce/permissions/permissions.html)
+
+### Active Directory (AD)
+
+A Microsoft based directory service for windows domain networks. It uses LDAP technology under the hood
+
+### Agile
+
+Building and delivering software in phases/parts rather than trying to build everything at once then delivering to the user/client. The later is known as a WaterFall model
+
+### Application Lifecycle Management (ALM)
+
+Entire product lifecycle management process for an application. From requirements management, development and testing until deployment.
+
+### Artifactory
+
+Version control for binaries.
+
+### Artifacts
+
+objects (usually binary and large) created by a build process
+
+### Atlassian
+
+A company that develops software products for developers and project managers including Bitbucket, Jira, Hipchat, Confluence, Bamboo. See [Atlassian] (https://www.atlassian.com)
+
+### Audit Log
+
+*** Needs definition here
+
+### Auto Defined User Group
+
+User groups are a way of centralizing control over important management tasks, particularly access control and password policies.
+A simple example of such groups are the users and the admins groups.
+In most of the cases these groups are auto defined in terms of access, rules of usage, conditions to be part of, etc...
+
+### Bamboo
+
+Atlassian's CI tool similar to GitLab CI and Jenkins
+
+### Basic Subscription
+
+Entry level subscription for GitLab EE currently available in packs of 10 see [Basic subscription](https://about.gitlab.com/pricing/)
+
+### Bitbucket
+
+Atlassian's web hosting service for Git and Mercurial Projects i.e. GitLab.com competitor
+
+### Branch
+
+A branch is a parallel version of a repository. Allows you to work on the repository without you affecting the "master" branch. Allows you to make changes without affecting the current "live" version. When you have made all your changes to your branch you can then merge to the master and to make the changes fo "live".
+
+### Branded Login
+
+Having your own logo on your GitLab instance login page instead of the GitLab logo.
+
+### CEPH
+
+is a distributed object store and file system designed to provide excellent performance, reliability and scalability.
+
+### Clone
+
+A copy of a repository stored on your machine that allows you to use your own editor without being online, but still tracks the changes made remotely.
+
+### Code Review
+
+Examination of a progam's code. The main aim is to maintain high standards quality of code that is being shipped.
+
+### Code Snippet
+
+A small amount of code. Usually for the purpose of showing other developers how
+to do something specific or reproduce a problem.
+
+### Collaborator
+
+Person with read and write access to a repository who has been invited by repository owner.
+
+### Commit
+
+Is a change (revision) to a file, and also creates an ID that allows you to see revision history and who made the changes.
+
+### Community
+
+Everyone who is using GitLab
+
+### Confluence
+
+Atlassian's product for collaboration of documents and projects.
+
+### Continuous Deivery
+
+Continuous delivery is a series of practices designed to ensure that code can be rapidly and safely deployed to production by delivering every change to a production-like environment and ensuring business applications and services function as expected through rigorous automated testing.
+
+### Continuous Deployment
+
+Continuous deployment is the next step of continuous delivery: Every change that passes the automated tests is deployed to production automatically.
+
+### Continuous Integration
+
+A process that involves adding new code commits to source code with the combined code being run on an automated test to ensure that the changes do not break the software.
+
+### Contributor
+
+Term used to a person contributing to an Open Source Project.
+
+### Data Centre
+
+Atlassian product for High Availability.
+
+### Deploy Keys
+
+An SSH key stored on the your server that grants access to a single GitLab repository. This is used by a GitLab runner to clone a project's code so that tests can be run against the checked out code.
+
+### Developer
+
+For us (GitLab) this means a software developer, i.e. someone who makes software. It is also one of the levels of access in our multi level approval system.
+
+### Diff
+
+Is the difference between two commits, or saved changes. This will also be shown visually after the changes.
+
+### Docker
+
+Containers wrap up a piece of software in a complete filesystem that contains everything it needs to run: code, runtime, system tools, system libraries – anything you can install on a server.
+This guarantees that it will always run the same, regardless of the environment it is running in.
+
+### Fork
+
+Your own copy of a repository that allows you to make changes to the repository without affecting the original.
+
+### Gerrit
+
+A code review tool built on top of Git.
+
+### Git Hooks
+
+Are scripts you can use to trigger actions at certain points.
+
+### GitHost.io
+
+Is a single-tenant solution that provides GitLab CE or EE as a managed service. GitLab Inc. is responsible for
+installing, updating, hosting, and backing up customers own private and secure GitLab instance.
+
+### GitHub
+
+A web-based Git repository hosting service with an enterprise offering. Its main features are: issue tracking, pull request with code review, abundancy of integrations and wiki. As of April 2016, the service has over 14 million users. It offers free public repos, private repos and enterprise services are paid.
+
+### GitLab CE
+
+Our free on Premise solution with >100,000 users
+
+### GitLab CI
+
+Our own Continuos Integration feature that is shipped with each instance
+
+### GitLab EE
+
+Our premium on premise solution that currently has Basic, Standard and Plus subscription packages with additional features and support.
+
+### GitLab.com
+
+Our free SaaS for public and private repositories.
+
+### Gitolite
+
+Is basically an access layer that sits on top of Git. Users are granted access to repos via a simple config file and you as an admin only needs the users public SSH key and a username from the user.
+
+### Gitorious
+
+A web based hosting service for projects using Git. It was acquired by GitLab and we discontinued the service. [Gitorious Acquisition Blog Post](https://about.gitlab.com/2015/03/03/gitlab-acquires-gitorious/)
+
+### HADR
+
+Sometimes written HA/DR. High Availability for Disaster Recovery. Usually refers to a strategy having a failover server in place in case the main server fails.
+
+### Hip Chat
+
+Atlassian's real time chat application for teams. Competitor to Slack, RocketChat and MatterMost.
+
+### High Availability
+
+Refers to a system or component that is continuously operational for a desirably long length of time. Availability can be measured relative to "100% operational" or "never failing."
+
+### Issue Tracker
+
+A tool used to manage, organize, and maintain a list of issues, making it easier for an organization to manage.
+
+### Jenkins
+
+An Open Source CI tool written using the Java programming language. Does the same job as GitLab CI, Bamboo, Travis CI. It is extremely popular. see [Jenkins](https://jenkins-ci.org/)
+
+### Jira
+
+Atlassian's project management software. i.e. a complex issue tracker. See[Jira](https://www.atlassian.com/software/jira)
+
+### Kerberos
+
+A network authentication protocol that uses secret-key cryptography for security.
+
+### Kubernetes
+
+An open source container cluster manager originally designed by Google. It's basically a platform for automating deployment, scaling, and operations of application containers over clusters of hosts.
+
+### Labels
+
+An identifier to describe a group of one or more specific file revisions
+
+### LDAP
+
+Lightweight Directory Access Protocol - basically its a directory (electronic address book) with user information e.g. name, phone_number etc
+
+### LDAP User Authentication
+
+Allowing GitLab to sign in people from an LDAP server i.e. Allow people whose names are on the electronic user directory server) to be able to use their LDAP accounts to login.
+
+### LDAP Group Sync
+
+Allows you to synchronize the members of a GitLab group with one or more LDAP groups.
+
+### Git LFS
+
+Git Large File Storage. A way to enable git to handle large binary files by using reference pointers within small text files to point to the large files.
+
+### Linux
+
+An operating system like Windows or OS X. It is mostly used by software developers and on servers.
+
+### Markdown
+
+Is a lightweight markup language with plain text formatting syntax designed so that it can be converted to HTML and many other formats using a tool by the same name.
+Markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor.
+
+### Maria DB
+
+A community developed fork/variation of MySQL. MySQL is owned by Oracle.
+
+### Master
+
+Name of the default branch in every git repository.
+
+### Mercurial
+
+A free distributed version control system like Git. Think of it as a competitor to Git.
+
+### Merge
+
+Takes changes from one branch, and applies them into another branch.
+
+### Meteor
+
+A hip platform for building javascript apps.[Meteor] (https://www.meteor.com)
+
+### Milestones
+
+Allows you to track the progress on issues, and merge requests, which allows you to get a snapshot of the progress made.
+
+### Mirror Repositories
+
+You can set up a project to automatically have its branches, tags, and commits updated from an upstream repository. This is useful when a repository you're interested in is located on a different server, and you want to be able to browse its content and its activity using the familiar GitLab interface.
+
+### MIT License
+
+A type of software license. It lets people do anything with your code with proper attribution and without warranty. It is the most common license for open source applications written in Ruby on Rails. GitLab CE is issued under this license.
+This means, you can download the code, modify it as you want even build a new commercial product using the underlying code and its not illegal. The only condition is that there is no form of waranty provided by GitLab so whatever happens if you use the code is your own problem.
+
+### Mondo
+
+*** Needs definition here
+
+### Multi LDAP Server
+
+*** Needs definition here
+
+### My SQL
+
+A relational database. Currently only supported if you are using EE. It is owned by Oracle.
+
+### Namespace
+
+In computing, a namespace is a set of symbols that are used to organize objects of various kinds, so that these objects may be referred to by name.
+
+Prominent examples include:
+- file systems are namespaces that assign names to files;
+- programming languages organize their variables and subroutines in namespaces;
+- computer networks and distributed systems assign names to resources, such as computers, printers, websites, (remote) files, etc.
+
+### Nginx
+
+(pronounced "engine x") is a web server. It can act as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache.
+
+### oAuth
+
+Is an open standard for authorization, commonly used as a way for Internet users to log into third party websites using their Microsoft, Google, Facebook or Twitter accounts without exposing their password.
+
+### Omnibus Packages
+
+Omnibus is a way to package the different services and tools required to run GitLab, so that users can install it without as much work.
+
+### On Premise
+
+On your own server. In GitLab, this refers to the ability to download GitLab EE/GitLab CE and host it on your own server rather than using GitLab.com which is hosted by GitLab Inc's servers.
+
+### Open Source Software
+
+Software for which the original source code is freely available and may be redistributed and modified.
+
+### Owner
+
+This is the most powerful person on a GitLab project. He has the permissions of all the other users plus the additional permission of being able to destroy i.e. delete the project
+
+### PaaS
+
+Typically referred to in regards to application development, it is a model in which a cloud provider delivers hardware and software tools to its users as a service
+
+### Perforce
+
+The company that produces Helix. A commercial, proprietary, centralised VCS well known for it's ability to version files of any size and type. They OEM a re-branded version of GitLab called "GitSwarm" that is tightly integrated with their "GitFusion" product, which in turn represents a portion of a Helix repository (called a depot) as a git repo
+
+### Phabricator
+
+Is a suite of web-based software development collaboration tools, including the Differential code review tool, the Diffusion repository browser, the Herald change monitoring tool, the Maniphest bug tracker and the Phriction wiki. Phabricator integrates with Git, Mercurial, and Subversion.
+
+### Piwik Analytics
+
+An open source analytics software to help you analyze web traffic. It is similar to google analytics only that google analytics is not open source and information is stored by google while in Piwik the information is stored in your own server hence fully private.
+
+### Plus Subscription
+
+GitLab Premium EE subscription that includes training and dedicated Account Management and Service Engineer and complete support package [Plus subscription](https://about.gitlab.com/pricing/)
+
+### PostgreSQL
+
+A relational database. Touted as the most advanced open source database.
+
+### Protected Branches
+
+A feature that protects branches from unauthorized pushes, force pushing or deletion.
+
+### Pull
+
+Git command to synchronize the local repository with the remote repository, by fetching all remote changes and merging them into the local repository.
+
+### Puppet
+
+A popular devops automation tool
+
+### Push
+
+Git command to send commits from the local repository to the remote repository.
+
+### RE Read Only
+
+Permissions to see a file and it's contents, but not change it
+
+### Rebase
+
+Moves a branch from one commit to another. This allows you to re-write your project's history.
+
+### Git Repository
+
+Storage location of all files which are tracked by git.
+
+### Requirements management
+
+*** Needs definition here
+
+### Revision
+
+*** Needs definition here
+
+### Revision Control
+
+Also known as version control or source control, is the management of changes to documents, computer programs, large web sites, and other collections of information. Changes are usually identified by a number or letter code, termed the "revision number", "revision level", or simply "revision".
+
+### RocketChat
+
+An open source chat application for teams. Very similar to Slack only that is is open-source.
+
+### Runners
+
+Actual build machines/containers that run/execute tests you have specified to be run on GitLab CI
+
+### SaaS
+
+Software as a service. Software is hosted centrally and accessed on-demand i.e. when you want to. This refers to GitLab.com in our scenario
+
+### SCM
+
+Software Configuration Management. Often used by people when they mean Version Control
+
+## Scrum
+
+An Agile framework designed to help complete complex (typically) software projects. It's made up of several parts: product requirments backlog, sprint plannnig, sprint (development), sprint review, retrospec (analyzing the sprint). The goal is to end up with potentially shippable products.
+
+### Scrum Board
+
+The board used to track the status and progress of each of the sprint backlog items.
+
+### Slack
+
+Real time messaging app for teams. Used internally by GitLab
+
+### Slave Servers
+
+Also known as secondary servers. They help to spread the load over multiple machines, they also provide backups when the master/primary server crashes.
+
+### Source Code
+
+Program code as typed by a computer programmer. i.e. it has not yet been compiled/translated by the computer to machine language.
+
+### SSH Key
+
+A unique identifier of a computer. It is used to identify computers without the need for a password. e.g. On GitLab I have added the ssh key of all my work machines so that the GitLab instance knows that it can accept code pushes and pulls from this trusted machines whose keys are I have added.
+
+### SSO
+
+Single Sign On. An authentication process that allows you enter one username and password to access multiple applications.
+
+### Standard Subscription
+
+Our mid range EE subscription that includes 24/7 support, support for High Availability [Standard Subscription](https://about.gitlab.com/pricing/)
+
+### Stash
+
+Atlassian's Git On-Premises solution. Think of it as Atlassian's GitLab EE. It is now known as BitBucket Server.
+
+### Subversion
+
+Non-proprietary, centralized version control system.
+
+### Sudo
+
+A program that allows you to perform superuser/administrator actions on Unix Operating Systems e.g. Linux, OS X. It actually stands for 'superuser do'
+
+### SVN
+
+Abbreviation for Subversion.
+
+### Tag
+
+Represents a version of a particular branch at a moment in time.
+
+### Tool Stack
+
+Set of tools used in a process to achieve a common outcome. E.g. set of tools used in Application Lifecycle Management.
+
+### Trac
+
+An Open Source project management and bug tracking web application.
+
+### User
+
+Anyone interacting with the software.
+
+### VCS
+
+Version Control Software
+
+### Waterfall
+
+A model of building software that involves collecting all requirements from the customer, then building and refining all the requirements and finally delivering the COMPLETE software to the customer that meets all the requirements specified by the customer
+
+### Webhooks
+
+A way for for an app to provide other applications with real-time information. e.g. send a message to a slack channel when a commit is pushed
+
+### Wiki
+
+A website/system that allows for collaborative editing of its content by the users. In programming, they usually contain documentation of how to use the software
diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md
new file mode 100644
index 00000000000..088f1cd7290
--- /dev/null
+++ b/doc/university/high-availability/aws/README.md
@@ -0,0 +1,387 @@
+
+# High Availability on AWS
+
+GitLab on AWS can leverage many of the services that are already
+configurable with High Availability. These services have a lot of
+flexibility and are able to adopt to most companies, best of all is the
+ability to automate both vertical and horizontal scaling.
+
+In this article we'll go through a basic HA setup where we'll start by
+configuring our Virtual Private Cloud and subnets to later integrate
+services such as RDS for our database server and ElastiCache as a Redis
+cluster to finally manage them within an auto scaling group with custom
+scaling policies.
+
+***
+
+## Where to Start
+
+Login to your AWS account through the `My Account` dropdown on
+`https://aws.amazon.com` or through the URI assigned to your team such as
+`https://myteam.signin.aws.amazon.com/console/`. You'll start on the
+Amazon Web Services console from where we can choose all of the services
+we'll be using to configure our cloud infrastructure.
+
+***
+
+## Network
+
+We'll start by creating a VPC for our GitLab cloud infrastructure, then
+we can create subnets to have public and private instances in at least
+two AZs. Public subnets will require a Route Table keep an associated
+Internet Gateway.
+
+### VPC
+
+Start by looking for the VPC option on the web console. Now create a new
+VPC. We can use `10.0.0.0/16` for the CIDR block and leave tenancy as
+default if we don't require dedicated hardware.
+
+![New VPC](img/new_vpc.png)
+
+If you're setting up the Elastic File System service then select the VPC
+and from the Actions dropdown choose Edit DNS Hostnames and select Yes.
+
+### Subnet
+
+Now let's create some subnets in different Availability Zones. Make sure
+that each subnet is associated the the VPC we just created, that it has
+a distinct VPC and lastly that CIDR blocks don't overlap. This will also
+allow us to enable multi AZ for redundancy.
+
+We will create private and public subnets to match load balancers and
+RDS instances as well.
+
+![Subnet Creation](img/subnet.png)
+
+The subnets are listed with their name, AZ and CIDR block:
+
+* gitlab-public-10.0.0.0 - us-west-2a - 10.0.0.0
+* gitlab-private-10.0.1.0 - us-west-2a - 10.0.1.0
+* gitlab-public-10.0.2.0 - us-west-2b - 10.0.2.0
+* gitlab-private-10.0.3.0 - us-west-2b - 10.0.3.0
+
+### Route Table
+
+Up to now all our subnets are private. We need to create a Route Table
+to associate an Internet Gateway. On the same VPC dashboard choose
+Route Tables on the left column and give it a name and associate it to
+our newly created VPC.
+
+![Route Table](img/route_table.png)
+
+
+### Internet Gateway
+
+Now still on the same dashboard head over to Internet Gateways and
+create a new one. After its created pres on the `Attach to VPC` button and
+select our VPC.
+
+![Internet Gateway](img/ig.png)
+
+### Configure Subnets
+
+Go back to the Router Tables screen and select the newly created one,
+press the Routes tab on the bottom section and edit it. We need to add a
+new target which will be our Internet Gateway and have it receive
+traffic from any destination.
+
+![Subnet Config](img/ig-rt.png)
+
+Before leaving this screen select the next tab to the rgiht which is
+Subnet Associations and add our public subnets. If you followed our
+naming convention they should be easy to find.
+
+***
+
+## Database with RDS
+
+For our database server we will use Amazon RDS which offers Multi AZ
+for redundancy. Lets start by creating a subnet group and then we'll
+create the actual RDS instance.
+
+### Subnet Group
+
+From the RDS dashboard select Subnet Groups. Lets select our VPC from
+the VPC ID dropdown and at the bottom we can add our private subnets.
+
+![Subnet Group](img/db-subnet-group.png)
+
+### RDS
+
+Select the RDS service from the Database section and create a new
+PostgreSQL instance. After choosing between a Production or
+Development instance we'll start with the actual configuration. On the
+image bellow we have the settings for this article but note the
+following two options which are of particular interest for HA:
+
+1. Multi-AZ-Deployment is recommended as redundancy. Read more at
+[High Availability (Multi-AZ)](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html)
+1. While we chose a General Purpose (SSD) for this article a Provisioned
+IOPS (SSD) is best suited for HA. Read more about it at
+[Storage for Amazon RDS](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html)
+
+![RDS Instance Specs](img/instance_specs.png)
+
+The rest of the setting on this page request a DB identifier, username
+and a master password. We've chosen to use `gitlab-ha`, `gitlab` and a
+very secure password respectively. Keep these in hand for later.
+
+![Network and Security](img/rds-net-opt.png)
+
+Make sure to choose our gitlab VPC, our subnet group, not have it public,
+and to leave it to create a new security group. The only additional
+change which will be helpful is the database name for which we can use
+`gitlabhq_production`.
+
+***
+
+## ElastiCache
+
+EC is an in-memory hosted caching solution. Redis maintains its own
+persistance and is used for certain types of application.
+
+Let's choose the ElastiCache service in the Database section from our
+AWS console. Now lets create a cache subnet group which will be very
+similar to the RDS subnet group. Make sure to select our VPC and its
+private subnets.
+
+![ElastiCache](img/ec-subnet.png)
+
+Now press the Launch a Cache Cluster and choose Redis for our
+DB engine. You'll be able to configure details such as replication,
+Multi AZ and node types. The second section will allow us to choose our
+subnet and security group and
+
+![Redis Cluster details](img/redis-cluster-det.png)
+
+![Redis Network](img/redis-net.png)
+
+***
+
+## Elastic File System
+
+This new AWS offering allows us to create a file system accessible by

+EC2 instances within a VPC. Choose our VPC and the subnets will be
+
automatically configured assuming we don't need to set explicit IPs.
+The
next section allows us to add tags and choose between General
+Purpose or
Max I/O which is a good option when being accessed by a
+large number of
EC2 instances.
+
+

![Elastic File System](img/elastic-file-system.png)
+
+To actually mount and install the NFS client we'll use the User Data
+section when adding our Launch Configuration.
+
+***
+
+## Initiate AMI
+
+We are going to launch an EC2 instance and bake an image so that we can
+later use it for auto scaling. We'll also take this opportunity to add an
+extension to our RDS through this temporary EC2 instance.
+
+### EC2 Instance
+
+Look for the EC2 option and choose to create an instance. We'll need at
+least a t2.medium type and for this article we'll choose an Ubuntu 14.04
+HVM 64-bit. In the Configure Instance section choose our GitLab VPC and
+a public subnet. I'd choose at least 10GB of storage.
+
+In the security group we'll create a new one considering that we need to
+SSH into the instance and also try it out through http. So let's add the
+http traffic from anywhere and name it something such as
+`gitlab-ec2-security-group`.
+
+While we wait for it to launch we can allocate an Elastic IP and
+associate it with our new EC2 instance.
+
+### RDS and Redis Security Group
+
+After the instance is being created we will navigate to our EC2 security
+groups and add a small change for our EC2 instances to be able to
+connect to RDS. First copy the security group name we just defined,
+namely `gitlab-ec2-security-group`, and edit select the RDS security
+group and edit the inbound rules. Choose the rule type to be PostgreSQL
+and paste the name under source.
+
+![RDS security group](img/rds-sec-group.png)
+
+Similar to the above we'll jump to the `gitlab-ec2-security-group` group
+and add a custom TCP rule for port 6379 accessible within itself.
+
+### Install GitLab
+
+To connect through SSH you will need to have the `pem` file which you
+chose available and with the correct permissions such as `400`.
+
+After accessing your server don't forget to update and upgrade your
+packages.
+
+ sudo apt-get update && sudo apt-get upgrade -y
+
+Then follow installation instructions from
+[GitLab](https://about.gitlab.com/downloads-ee/#ubuntu1404), but before
+running reconfigure we need to make sure all our services are tied down
+so just leave the reconfigure command until after we edit our gitlab.rb
+file.
+
+
+### Extension for PostgreSQL
+
+Connect to your new RDS instance to verify access and to install
+a required extension. We can find the host or endpoint by selecting the
+instance and we just created and after the details drop down we'll find
+it labeled as 'Endpoint'; do remember not to include the colon and port
+number.
+
+ sudo /opt/gitlab/embedded/bin/psql -U gitlab -h <rds-endpoint> -d gitlabhq_production
+ psql (9.4.7)
+ Type "help" for help.
+
+ gitlab=# CREATE EXTENSION pg_trgm;
+ gitlab=# \q
+
+### Configure GitLab
+
+While connected to your server edit the `gitlab.rb` file at `/etc/gitlab/gitlab.rb`
+find the `external_url 'http://gitlab.example.com'` option and change it
+to the domain you will be using or the public IP address of the current
+instance to test the configuration.
+
+For a more detailed description about configuring GitLab read [Configuring GitLab for HA](http://docs.gitlab.com/ee/administration/high_availability/gitlab.html)
+
+Now look for the GitLab database settings and uncomment as necessary. In
+our current case we'll specify the adapter, encoding, host, db name,
+username, and password.
+
+ gitlab_rails['db_adapter'] = "postgresql"
+ gitlab_rails['db_encoding'] = "unicode"
+ gitlab_rails['db_database'] = "gitlabhq_production"
+ gitlab_rails['db_username'] = "gitlab"
+ gitlab_rails['db_password'] = "mypassword"
+ gitlab_rails['db_host'] = "<rds-endpoint>"
+
+Next we only need to configure the Redis section by adding the host and
+uncommenting the port.
+
+
+
+The last configuration step is to [change the default file locations ](http://docs.gitlab.com/ee/administration/high_availability/nfs.html)
+to make the EFS integration easier to manage.
+
+ gitlab_rails['redis_host'] = "<redis-endpoint>"
+ gitlab_rails['redis_port'] = 6379
+
+Finally run reconfigure, you might find it useful to run a check and
+a service status to make sure everything has been setup correctly.
+
+ sudo gitlab-ctl reconfigure
+ sudo gitlab-rake gitlab:check
+ sudo gitlab-ctl status
+
+If everything looks good copy the Elastic IP over to your browser and
+test the instance manually.
+
+### AMI
+
+After you finish testing your EC2 instance go back to its dashboard and
+while the instance is selected press on the Actions dropdown to choose
+Image -> Create an Image. Give it a name and description and confirm.
+
+***
+
+## Load Balancer
+
+On the same dashboard look for Load Balancer on the left column and press
+the Create button. Choose a classic Load Balancer, our gitlab VPC, not
+internal and make sure its listening for HTTP and HTTPS on port 80.
+
+Here is a tricky part though, when adding subnets we need to associate
+public subnets instead of the private ones where our instances will
+actually live.
+
+On the secruity group section let's create a new one named
+`gitlab-loadbalancer-sec-group` and allow both HTTP ad HTTPS traffic
+from anywhere.
+
+The Load Balancer Health will allow us to indicate where to ping and what
+makes up a healthy or unhealthy instance.
+
+We won't add the instance on the next session because we'll destroy it
+momentarily as we'll be using the image we where creating. We will keep
+the Enable Cross-Zone and Enable Connection Draining active.
+
+After we finish creating the Load Balancer we can re visit our Security
+Groups to improve access only through the ELB and any other requirement
+you might have.
+
+***
+
+## Auto Scaling Group
+
+Our AMI should be done by now so we can start working on our Auto
+Scaling Group.
+
+This option is also available through the EC2 dashboard on the left
+sidebar. Press on the create button. Select the new image on My AMIs and
+give it a `t2.medium` size. To be able to use Elastic File System we need
+to add a script to mount EFS automatically at launch. We'll do this at
+the Advanced Details section where we have a [User Data](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html)
+text area that allows us to add a lot of custom configurations which
+allows you to add a custom script for when launching an instance. Let's
+add the following script to the User Data section:
+
+
+ #cloud-config
+ package_upgrade: true
+ packages:
+ - nfs-common
+ runcmd:
+ - mkdir -p /gitlab-data
+ - chown ec2-user:ec2-user /gitlab-data
+ - echo "$(curl --silent http://169.254.169.254/latest/meta-data/placement/availability-zone).file-system-id.aws-region.amazonaws.com:/ /gitlab-data nfs defaults,vers=4.1 0 0" >> /etc/fstab
+ - mount -a -t nfs
+ - sudo gitlab-ctl reconfigure
+
+On the security group section we can chosse our existing
+`gitlab-ec2-security-group` group which has already been tested.
+
+After this is launched we are able to start creating our Auto Scaling
+Group. Start by giving it a name and assinging it our VPC and private
+subnets. We also want to always start with two instances and if you
+scroll down to Advanced Details we can choose to receive traffic from ELBs.
+Lets enable that option and select our ELB. We also want to use the ELB's
+health check.
+
+![Auto scaling](img/auto-scaling-det.png)
+
+### Policies
+
+This is the really great part of Auto Scaling, we get to choose when AWS
+launches new instances and when it removes them. For this group we'll
+scale between 2 and 4 instances where one instance will be added if CPU
+utilization is greater than 60% and one instance is removed if it falls
+to less than 45%. Here are the complete policies:
+
+![Policies](img/policies.png)
+
+You'll notice that after we save this AWS starts launching our two
+instances in different AZs and without a public IP which is exactly what
+we where aiming for.
+
+***
+
+## Final Thoughts
+
+After you're done with the policies section have some fun trying to break
+instances. You should be able to see how the Auto Scaling Group and the
+EC2 screen start bringing them up again.
+
+High Availability is a very big area, we went mostly through scaling and
+some redundancy options but it might also imply Geographic replication.
+There is a lot of ground yet to cover so have a read through these other
+resources and feel free to open an issue to request additional material.
+
+ * [GitLab High Availability](http://docs.gitlab.com/ce/administration/high_availability/README.html#sts=High Availability)
+ * [GitLab Geo](http://docs.gitlab.com/ee/gitlab-geo/README.html)
diff --git a/doc/university/high-availability/aws/img/auto-scaling-det.png b/doc/university/high-availability/aws/img/auto-scaling-det.png
new file mode 100644
index 00000000000..e9b65529495
--- /dev/null
+++ b/doc/university/high-availability/aws/img/auto-scaling-det.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/db-subnet-group.png b/doc/university/high-availability/aws/img/db-subnet-group.png
new file mode 100644
index 00000000000..0768aa73c45
--- /dev/null
+++ b/doc/university/high-availability/aws/img/db-subnet-group.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/ec-subnet.png b/doc/university/high-availability/aws/img/ec-subnet.png
new file mode 100644
index 00000000000..f41d78b271d
--- /dev/null
+++ b/doc/university/high-availability/aws/img/ec-subnet.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/elastic-file-system.png b/doc/university/high-availability/aws/img/elastic-file-system.png
new file mode 100644
index 00000000000..7de866d1e89
--- /dev/null
+++ b/doc/university/high-availability/aws/img/elastic-file-system.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/ig-rt.png b/doc/university/high-availability/aws/img/ig-rt.png
new file mode 100644
index 00000000000..93bb0c2ae02
--- /dev/null
+++ b/doc/university/high-availability/aws/img/ig-rt.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/ig.png b/doc/university/high-availability/aws/img/ig.png
new file mode 100644
index 00000000000..cc50456370f
--- /dev/null
+++ b/doc/university/high-availability/aws/img/ig.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/instance_specs.png b/doc/university/high-availability/aws/img/instance_specs.png
new file mode 100644
index 00000000000..ef31dc41dae
--- /dev/null
+++ b/doc/university/high-availability/aws/img/instance_specs.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/new_vpc.png b/doc/university/high-availability/aws/img/new_vpc.png
new file mode 100644
index 00000000000..4aac6af7c7a
--- /dev/null
+++ b/doc/university/high-availability/aws/img/new_vpc.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/policies.png b/doc/university/high-availability/aws/img/policies.png
new file mode 100644
index 00000000000..8c58117e4fa
--- /dev/null
+++ b/doc/university/high-availability/aws/img/policies.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/rds-net-opt.png b/doc/university/high-availability/aws/img/rds-net-opt.png
new file mode 100644
index 00000000000..bc204de2474
--- /dev/null
+++ b/doc/university/high-availability/aws/img/rds-net-opt.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/rds-sec-group.png b/doc/university/high-availability/aws/img/rds-sec-group.png
new file mode 100644
index 00000000000..8864dc3e463
--- /dev/null
+++ b/doc/university/high-availability/aws/img/rds-sec-group.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/redis-cluster-det.png b/doc/university/high-availability/aws/img/redis-cluster-det.png
new file mode 100644
index 00000000000..9e9a81283c5
--- /dev/null
+++ b/doc/university/high-availability/aws/img/redis-cluster-det.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/redis-net.png b/doc/university/high-availability/aws/img/redis-net.png
new file mode 100644
index 00000000000..037bd6d6897
--- /dev/null
+++ b/doc/university/high-availability/aws/img/redis-net.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/route_table.png b/doc/university/high-availability/aws/img/route_table.png
new file mode 100644
index 00000000000..1dea322474d
--- /dev/null
+++ b/doc/university/high-availability/aws/img/route_table.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/subnet.png b/doc/university/high-availability/aws/img/subnet.png
new file mode 100644
index 00000000000..dbc71201992
--- /dev/null
+++ b/doc/university/high-availability/aws/img/subnet.png
Binary files differ
diff --git a/doc/university/process/README.md b/doc/university/process/README.md
new file mode 100644
index 00000000000..7ff53c2cc3f
--- /dev/null
+++ b/doc/university/process/README.md
@@ -0,0 +1,30 @@
+---
+title: University | Process
+---
+
+## Suggesting improvements
+
+If you would like to teach a class or participate or help in any way please
+submit a merge request and assign it to [Job](https://gitlab.com/u/JobV).
+
+If you have suggestions for additional courses you would like to see,
+please submit a merge request to add an upcoming class, assign to
+[Chad](https://gitlab.com/u/chadmalchow) and /cc [Job](https://gitlab.com/u/JobV).
+
+## Adding classes
+
+1. All training materials of any kind should be added to [GitLab CE](https://gitlab.com/gitlab-org/gitlab-ce/)
+ to ensure they are available to a broad audience (don't use any other repo or
+ storage for training materials).
+1. Don't make materials that are needlessly specific to one group of people, try
+ to keep the wording broad and inclusive (don't make things for only GitLab Inc.
+ people, only interns, only customers, etc.).
+1. To allow people to contribute all content should be in git.
+1. The content can go in a subdirectory under `/doc/university/`.
+1. To make, view or modify the slides of the classes use [Deckset](http://www.decksetapp.com/)
+ or [RevealJS](http://lab.hakim.se/reveal-js/). Do not use Powerpoint or Google
+ Slides since this prevents everyone from contributing.
+1. Please upload any video recordings to our Youtube channel. We prefer them to
+ be public, if needed they can be unlisted but if so they should be linked from
+ this page.
+1. Please create a merge request and assign to [SeanPackham](https://gitlab.com/u/SeanPackham).
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
new file mode 100644
index 00000000000..da991e56370
--- /dev/null
+++ b/doc/university/support/README.md
@@ -0,0 +1,188 @@
+
+## Support Boot Camp
+
+**Goal:** Prepare new Service Engineers at GitLab
+
+For each stage there are learning goals and content to support the learning of the engineer.
+The goal of this boot camp is to have every Service Engineer prepared to help our customers
+with whatever needs they might have and to also assist our awesome community with their
+questions.
+
+Always start with the [University Overview](../README.md) and then work
+your way here for more advanced and specific training. Once you feel comfortable
+with the topics of the current stage, move to the next.
+
+### Stage 1
+
+Follow the topics on the [University Overview](../README.md), concentrate on it
+during your first Stage, but also:
+
+- Perform the [first steps](https://about.gitlab.com/handbook/support/onboarding/#first-steps) of
+ the on-boarding process for new Service Engineers
+
+#### Goals
+
+Aim to have a good overview of the Product and main features, Git and the Company
+
+### Stage 2
+
+Continue to look over remaining portions of the [University Overview](../README.md) and continue on to these topics:
+
+#### Set up your development machine
+
+Get your development machine ready to familiarize yourself with the codebase, the components, and to be prepared to reproduce issues that our users encounter
+
+- Install the [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit)
+ - [Setup OpenLDAP as part of this](https://gitlab.com/gitlab-org/gitlab-development-kit#openldap)
+
+#### Become comfortable with the Installation processes that we support
+
+It's important to understand how to install GitLab in the same way that our users do. Try installing different versions and upgrading and downgrading between them. Installation from source will give you a greater understanding of the components that we employ and how everything fits together.
+
+Sometimes we need to upgrade customers from old versions of GitLab to latest, so it's good to get some experience of doing that now.
+
+- [Installation Methods](https://about.gitlab.com/installation/):
+ - [Omnibus](https://gitlab.com/gitlab-org/omnibus-gitlab/)
+ - [Docker](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/docker)
+ - [Source](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md)
+- Get yourself a Digital Ocean droplet, where you can install and maintain your own instance of GitLab
+ - Ask in #infrastructure about this
+ - Populate with some test data
+ - Keep this up-to-date as patch and version releases become available, just like our customers would
+- Try out the following installation path
+ - [Install GitLab 4.2 from source](https://gitlab.com/gitlab-org/gitlab-ce/blob/d67117b5a185cfb15a1d7e749588ff981ffbf779/doc/install/installation.md)
+ - External MySQL database
+ - External NGINX
+ - Create some test data
+ - Populated Repos
+ - Users
+ - Groups
+ - Projects
+ - [Backup using our Backup rake task](http://docs.gitlab.com/ce/raketasks/backup_restore.html#create-a-backup-of-the-gitlab-system)
+ - [Upgrade to 5.0 source using our Upgrade documentation](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/4.2-to-5.0.md)
+ - [Upgrade to 5.1 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/5.0-to-5.1.md)
+ - [Upgrade to 6.0 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/5.1-to-6.0.md)
+ - [Upgrade to 7.14 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/6.x-or-7.x-to-7.14.md)
+ - [Backup using our Backup rake task](http://docs.gitlab.com/ce/raketasks/backup_restore.html#create-a-backup-of-the-gitlab-system)
+ - [Perform the MySQL to PostgreSQL migration to convert your backup](http://docs.gitlab.com/ce/update/mysql_to_postgresql.html#converting-a-gitlab-backup-file-from-mysql-to-postgres)
+ - [Upgrade to Omnibus 7.14](http://doc.gitlab.com/omnibus/update/README.html#upgrading-from-a-non-omnibus-installation-to-an-omnibus-installation)
+ - [Restore backup using our Restore rake task](http://docs.gitlab.com/ce/raketasks/backup_restore.html#restore-a-previously-created-backup)
+ - [Upgrade to latest EE](https://about.gitlab.com/downloads-ee)
+ - (GitLab inc. only) Acquire and apply a license for the Enterprise Edition product, ask in #support
+- Perform a downgrade from [EE to CE](http://doc.gitlab.com/ee/downgrade_ee_to_ce/README.html)
+
+#### Start to learn about some of the integrations that we support
+
+Our integrations add great value to GitLab. User questions often relate to integrating GitLab with existing external services and the configuration involved
+
+- Learn about our Integrations (specially, not only):
+ - [LDAP](http://doc.gitlab.com/ee/integration/ldap.html)
+ - [JIRA](http://doc.gitlab.com/ee/project_services/jira.html)
+ - [Jenkins](http://doc.gitlab.com/ee/integration/jenkins.html)
+ - [SAML](http://doc.gitlab.com/ce/integration/saml.html)
+
+#### Goals
+
+- Aim to be comfortable with installation of the GitLab product and configuration of some of the major integrations
+- Aim to have an installation available for reproducing customer reports
+
+### Stage 3
+
+#### Understand the gathering of diagnostics for GitLab instances
+
+- Learn about the GitLab checks that are available
+ - [Environment Information and maintenance checks](http://docs.gitlab.com/ce/raketasks/maintenance.html)
+ - [GitLab check](http://docs.gitlab.com/ce/raketasks/check.html)
+ - Omnibus commands
+ - [Status](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#get-service-status)
+ - [Starting and stopping services](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#starting-and-stopping)
+ - [Starting a rails console](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#invoking-rake-tasks)
+
+#### Learn about the Support process
+
+Zendesk is our Support Centre and our main communication line with our Customers. We communicate with customers through several other channels too
+
+- Familiarize yourself with ZenDesk
+ - [UI Overview](https://support.zendesk.com/hc/en-us/articles/203661806-Introduction-to-the-Zendesk-agent-interface)
+ - [Updating Tickets](https://support.zendesk.com/hc/en-us/articles/212530318-Updating-and-solving-tickets)
+ - [Working w/ Tickets](https://support.zendesk.com/hc/en-us/articles/203690856-Working-with-tickets) *Read: avoiding agent collision.*
+- Dive into our ZenDesk support process by reading how to [handle tickets](https://about.gitlab.com/handbook/support/onboarding/#handling-tickets)
+- Start getting real world experience by handling real tickets, all the while gaining further experience with the Product.
+ - First, learn about our [Support Channels](https://about.gitlab.com/handbook/support/#support-channels)
+ - Ask other Service Engineers for help, when necessary, and to review your responses
+ - Start with [StackOverflow](https://about.gitlab.com/handbook/support/#stack-overflowa-namestack-overflowa) and the [GitLab forum](https://about.gitlab.com/handbook/support/#foruma-namegitlab-foruma)
+ - Here you will find a large variety of queries mainly from our Users who are self hosting GitLab CE
+ - Understand the questions that are asked and dig in to try to find a solution
+ - [Proceed on to the GitLab.com Support Forum](https://about.gitlab.com/handbook/support/#gitlabcom-support-trackera-namesupp-foruma)
+ - Here you will find queries regarding our own GitLab.com
+ - Helping Users here will give you an understanding of our Admin interface and other tools
+ - [Proceed on to the Twitter tickets in Zendesk](https://about.gitlab.com/handbook/support/#twitter)
+ - Here you will gain a great insight into our userbase
+ - Learn from any complaints and problems and feed them back to the team
+ - Tweets can range from help needed with GitLab installations, the API and just general queries
+ - [Proceed on to Regular email Support tickets](https://about.gitlab.com/handbook/support/#regular-zendesk-tickets-a-nameregulara)
+ - Here you will find tickets from our GitLab EE Customers and GitLab CE Users
+ - Tickets here are extremely varied and often very technical
+ - You should be prepared for these tickets, given the knowledge gained from previous tiers and your training
+- Check out your colleagues' responses
+ - Hop on to the #support-live-feed channel in Slack and see the tickets as they come in and are updated
+ - Read through old tickets that your colleagues have worked on
+- Start arranging to pair on calls with other Service Engineers. Aim to cover a few of each type of call
+ - [Learn about Cisco WebEx](https://about.gitlab.com/handbook/support/onboarding/#webexa-namewebexa)
+ - Training calls
+ - Information gathering calls
+ - It's good to find out how new and prospective customers are going to be using the product and how they will set up their infrastructure
+ - Diagnosis calls
+ - When email isn't enough we may need to hop on a call and do some debugging along side the customer
+ - These paired calls are a great learning experience
+ - Upgrade calls
+ - Emergency calls
+
+#### Learn about the Escalation process for tickets
+
+Some tickets need specific knowledge or a deep understanding of a particular component and will need to be escalated to a Senior Service Engineer or Developer
+
+- Read about [Escalation](https://about.gitlab.com/handbook/support/onboarding/#create-issuesa-namecreate-issuea)
+- Find the macros in Zendesk for ticket escalations
+- Take a look at the [GitLab.com Team page](https://about.gitlab.com/team/) to find the resident experts in their fields
+
+#### Learn about raising issues and fielding feature proposals
+
+- Understand what's in the pipeline and proposed features at GitLab: [Direction Page](https://about.gitlab.com/direction/)
+- Practice searching issues and filtering using [labels](https://gitlab.com/gitlab-org/gitlab-ce/labels) to find existing feature proposals and bugs
+- If raising a new issue always provide a relevant label and a link to the relevant ticket in Zendesk
+- Add [customer labels](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=customer) for those issues relevant to our subscribers
+- Take a look at the [existing issue templates](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker) to see what is expected
+- Raise issues for bugs in a manner that would make the issue easily reproducible. A Developer or a contributor may work on your issue
+
+#### Goals
+
+- Aim to have a good understanding of the problems that customers are facing
+- Aim to have gained experience in scheduling and participating in calls with customers
+- Aim to have a good understanding of ticket flow through Zendesk and how to interat with our various channels
+
+### Stage 4
+
+#### Advanced GitLab topics
+
+Move on to understanding some of GitLab's more advanced features. You can make use of GitLab.com to understand the features from an end-user perspective and then use your own instance to understand setup and configuration of the feature from an Administrative perspective
+
+- Set up and try [Git Annex](http://doc.gitlab.com/ee/workflow/git_annex.html)
+- Set up and try [Git LFS](http://doc.gitlab.com/ee/workflow/lfs/manage_large_binaries_with_git_lfs.html)
+- Get to know the [GitLab API](http://doc.gitlab.com/ee/api/README.html), its capabilities and shortcomings
+- Learn how to [migrate from SVN to Git](http://doc.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
+- Set up [GitLab CI](http://doc.gitlab.com/ee/ci/quick_start/README.html)
+- Create your first [GitLab Page](http://doc.gitlab.com/ee/pages/administration.html)
+- Get to know the GitLab Codebase by reading through the source code:
+ - Find the differences between the [EE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
+ and the [CE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
+- Ask as many questions as you can think of on the `#support` chat channel
+
+#### Get initiated for on-call duty
+
+- Read over the [public run-books to understand common tasks](https://gitlab.com/gitlab-com/runbooks)
+- Create an issue on the internal Organization tracker to schedule time with the DevOps / Production team, so that you learn how to handle GitLab.com going down. Once you are trained for this, you are ready to be added to the on-call rotation.
+
+#### Goals
+
+- Aim to become a fully-fledged Service Engineer!
diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md
new file mode 100644
index 00000000000..03c62a81b10
--- /dev/null
+++ b/doc/university/training/end-user/README.md
@@ -0,0 +1,420 @@
+
+# Training
+
+This training material is the markdown used to generate training slides
+which can be found at [End User Slides](https://gitlab-org.gitlab.io/end-user-training-slides/#/)
+through it's [RevealJS](https://gitlab.com/gitlab-org/end-user-training-slides)
+project.
+
+---
+
+## Git Intro
+
+---
+
+### What is a Version Control System (VCS)
+
+- Records changes to a file
+- Maintains history of changes
+- Disaster Recovery
+- Types of VCS: Local, Centralized and Distributed
+
+---
+
+### Short Story of Git
+
+- 1991-2002: The Linux kernel was being maintaned by sharing archived files
+ and patches.
+- 2002: The Linux kernel project began using a DVCS called BitKeeper
+- 2005: BitKeeper revoked the free-of-charge status and Git was created
+
+---
+
+### What is Git
+
+- Distributed Version Control System
+- Great branching model that adapts well to most workflows
+- Fast and reliable
+- Keeps a complete history
+- Disaster recovery friendly
+- Open Source
+
+---
+
+### Getting Help
+
+- Use the tools at your disposal when you get stuck.
+ - Use `git help <command>` command
+ - Use Google (i.e. StackOverflow, Google groups)
+ - Read documentation at https://git-scm.com
+
+---
+
+## Git Setup
+Workshop Time!
+
+---
+
+### Setup
+
+- Windows: Install 'Git for Windows'
+ - https://git-for-windows.github.io
+- Mac: Type `git` in the Terminal application.
+ - If it's not installed, it will prompt you to install it.
+- Linux
+ - Debian: `sudo apt-get install git-all`
+ - Red Hat `sudo yum install git-all`
+
+---
+
+### Configure
+
+- One-time configuration of the Git client:
+
+```bash
+git config --global user.name "Your Name"
+git config --global user.email you@example.com
+```
+
+- If you don't use the global flag you can setup a different author for
+ each project
+- Check settings with:
+
+```bash
+git config --global --list
+```
+- You might want or be required to use an SSH key.
+ - Instructions: [SSH](http://doc.gitlab.com/ce/ssh/README.html)
+
+---
+
+### Workspace
+
+- Choose a directory on you machine easy to access
+- Create a workspace or development directory
+- This is where we'll be working and adding content
+
+---
+
+```bash
+mkdir ~/development
+cd ~/development
+
+-or-
+
+mkdir ~/workspace
+cd ~/workspace
+```
+
+---
+
+## Git Basics
+
+---
+
+### Git Workflow
+
+- Untracked files
+ - New files that Git has not been told to track previously.
+- Working area (Workspace)
+ - Files that have been modified but are not committed.
+- Staging area (Index)
+ - Modified files that have been marked to go in the next commit.
+- Upstream
+ - Hosted repository on a shared server
+
+---
+
+### GitLab
+
+- GitLab is an application to code, test and deploy.
+- Provides repository management with access controls, code reviews,
+ issue tracking, Merge Requests, and other features.
+- The hosted version of GitLab is gitlab.com
+
+---
+
+### New Project
+
+- Sign in into your gitlab.com account
+- Create a project
+- Choose to import from 'Any Repo by URL' and use https://gitlab.com/gitlab-org/training-examples.git
+- On your machine clone the `training-examples` project
+
+---
+
+### Git and GitLab basics
+
+1. Edit `edit_this_file.rb` in `training-examples`
+2. See it listed as a changed file (working area)
+3. View the differences
+4. Stage the file
+5. Commit
+6. Push the commit to the remote
+7. View the git log
+
+---
+
+```shell
+# Edit `edit_this_file.rb`
+git status
+git diff
+git add <file>
+git commit -m 'My change'
+git push origin master
+git log
+```
+
+---
+
+### Feature Branching
+
+1. Create a new feature branch called `squash_some_bugs`
+2. Edit `bugs.rb` and remove all the bugs.
+3. Commit
+4. Push
+
+---
+
+```shell
+git checkout -b squash_some_bugs
+# Edit `bugs.rb`
+git status
+git add bugs.rb
+git commit -m 'Fix some buggy code'
+git push origin squash_some_bugs
+```
+
+---
+
+## Merge Request
+
+---
+
+### Merge requests
+
+- When you want feedback create a merge request
+- Target is the ‘default’ branch (usually master)
+- Assign or mention the person you would like to review
+- Add `WIP` to the title if it's a work in progress
+- When accepting, always delete the branch
+- Anyone can comment, not just the assignee
+- Push corrections to the same branch
+
+
+---
+
+### Merge request example
+
+- Create your first merge request
+ - Use the blue button in the activity feed
+ - View the diff (changes) and leave a comment
+ - Push a new commit to the same branch
+ - Review the changes again and notice the update
+
+---
+
+### Feedback and Collaboration
+
+- Merge requests are a time for feedback and collaboration
+- Giving feedback is hard
+- Be as kind as possible
+- Receiving feedback is hard
+- Be as receptive as possible
+- Feedback is about the best code, not the person. You are not your code
+- Feedback and Collaboration
+
+---
+
+### Feedback and Collaboration
+
+- Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:[Thoughtbot](https://github.com/thoughtbot/guides/tree/master/code-review)
+- See GitLab merge requests for examples: [Merge Requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
+
+---
+
+## Merge Conflicts
+
+---
+
+### Merge Conflicts
+* Happen often
+* Learning to fix conflicts is hard
+* Practice makes perfect
+* Force push after fixing conflicts. Be careful!
+
+---
+
+### Example Plan
+1. Checkout a new branch and edit conflicts.rb. Add 'Line4' and 'Line5'.
+2. Commit and push
+3. Checkout master and edit conflicts.rb. Add 'Line6' and 'Line7' below 'Line3'.
+4. Commit and push to master
+5. Create a merge request and watch it fail
+6. Rebase our new branch with master
+7. Fix conflicts on the conflicts.rb file.
+8. Stage the file and continue rebasing
+9. Force push the changes
+10. Finally continue with the Merge Request
+
+---
+
+### Example 1/2
+
+ git checkout -b conflicts_branch
+
+ # vi conflicts.rb
+ # Add 'Line4' and 'Line5'
+
+ git commit -am "add line4 and line5"
+ git push origin conflicts_branch
+
+ git checkout master
+
+ # vi conflicts.rb
+ # Add 'Line6' and 'Line7'
+ git commit -am "add line6 and line7"
+ git push origin master
+
+---
+
+### Example 2/2
+
+Create a merge request on the GitLab web UI. You'll see a conflict warning.
+
+ git checkout conflicts_branch
+ git fetch
+ git rebase master
+
+ # Fix conflicts by editing the files.
+
+ git add conflicts.rb
+ # No need to commit this file
+
+ git rebase --continue
+
+ # Remember that we have rewritten our commit history so we
+ # need to force push so that our remote branch is restructured
+ git push origin conflicts_branch -f
+
+---
+
+### Notes
+
+* When to use `git merge` and when to use `git rebase`
+* Rebase when updating your branch with master
+* Merge when bringing changes from feature to master
+* Reference: https://www.atlassian.com/git/tutorials/merging-vs-rebasing/
+
+---
+
+## Revert and Unstage
+
+---
+
+### Unstage
+
+To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch:
+
+ git reset HEAD <file>
+
+This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use:
+
+ git checkout -- <file>
+
+To remove a file from disk and repo use 'git rm' and to rm a dir use the '-r' flag:
+
+ git rm '*.txt'
+ git rm -r <dirname>
+
+If we want to remove a file from the repository but keep it on disk, say we forgot to add it to our .gitignore file then use `--cache`:
+
+ git rm <filename> --cache
+
+---
+
+### Undo Commits
+
+Undo last commit putting everything back into the staging area:
+
+ git reset --soft HEAD^
+
+Add files and change message with:
+
+ git commit --amend -m "New Message"
+
+Undo last and remove changes
+
+ git reset --hard HEAD^
+
+Same as last one but for two commits back:
+
+ git reset --hard HEAD^^
+
+Don't reset after pushing
+
+---
+
+### Reset Workflow
+
+1. Edit file again 'edit_this_file.rb'
+2. Check status
+3. Add and commit with wrong message
+4. Check log
+5. Amend commit
+6. Check log
+7. Soft reset
+8. Check log
+9. Pull for updates
+10. Push changes
+
+----
+
+ # Change file edit_this_file.rb
+ git status
+ git commit -am "kjkfjkg"
+ git log
+ git commit --amend -m "New comment added"
+ git log
+ git reset --soft HEAD^
+ git log
+ git pull origin master
+ git push origin master
+
+---
+
+### Note
+
+git revert vs git reset
+Reset removes the commit while revert removes the changes but leaves the commit
+Revert is safer considering we can revert a revert
+
+
+ # Changed file
+ git commit -am "bug introduced"
+ git revert HEAD
+ # New commit created reverting changes
+ # Now we want to re apply the reverted commit
+ git log # take hash from the revert commit
+ git revert <rev commit hash>
+ # reverted commit is back (new commit created again)
+
+---
+
+## Questions
+
+---
+
+## Instructor Notes
+
+---
+
+### Version Control
+ - Local VCS was used with a filesystem or a simple db.
+ - Centralized VCS such as Subversion includes collaboration but
+ still is prone to data loss as the main server is the single point of
+ failure.
+ - Distributed VCS enables the team to have a complete copy of the project
+ and work with little dependency to the main server. In case of a main
+ server failing the project can be recovered by any of the latest copies
+ from the team
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index b058f8e2a03..b24d338e3e0 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -22,7 +22,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
### 3. Update Ruby
-If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible.
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
You can check which version you are running with `ruby -v`.
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index c8ca42f97bc..ee9fb1a2a68 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -22,7 +22,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
### 3. Update Ruby
-If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible.
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
You can check which version you are running with `ruby -v`.
@@ -70,7 +72,7 @@ sudo -u git -H git checkout 8-12-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.4.0
+sudo -u git -H git checkout v3.6.0
```
### 6. Update gitlab-workhorse
@@ -82,7 +84,7 @@ GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
-sudo -u git -H git checkout v0.7.11
+sudo -u git -H git checkout v0.8.2
sudo -u git -H make
```
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
new file mode 100644
index 00000000000..d61c76673f7
--- /dev/null
+++ b/doc/update/8.12-to-8.13.md
@@ -0,0 +1,201 @@
+# From 8.12 to 8.13
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 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. Update Ruby
+
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz
+echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz
+cd ruby-2.3.1
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. 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-13-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-13-stable-ee
+```
+
+### 5. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v3.6.2
+```
+
+### 6. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v0.8.2
+sudo -u git -H make
+```
+
+### 7. 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
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# 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
+```
+
+### 8. 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-12-stable:config/gitlab.yml.example origin/8-13-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+sudo -u git -H git config --global repack.writeBitmaps true
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-12-stable:lib/support/nginx/gitlab-ssl origin/8-13-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-12-stable:lib/support/nginx/gitlab origin/8-13-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+### 9. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 10. 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.12)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.11 to 8.12](8.11-to-8.12.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index c7fda8a497f..56e5b802a52 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -27,6 +27,7 @@
* [Horizontal Rule](#horizontal-rule)
* [Line Breaks](#line-breaks)
* [Tables](#tables)
+* [Footnotes](#footnotes)
**[Wiki-Specific Markdown](#wiki-specific-markdown)**
@@ -699,6 +700,15 @@ By including colons in the header row, you can align the text within that column
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
+## Footnotes
+
+You can add footnotes to your text as follows.[^1]
+[^1]: This is my awesome footnote.
+
+```
+You can add footnotes to your text as follows.[^1]
+[^1]: This is my awesome footnote.
+```
## Wiki-specific Markdown
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 1498cb361c8..c0dc80325b6 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -23,6 +23,7 @@ The following table depicts the various user permission levels in a project.
| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ |
| Pull project code | | ✓ | ✓ | ✓ | ✓ |
| Download project | | ✓ | ✓ | ✓ | ✓ |
| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
@@ -63,7 +64,7 @@ The following table depicts the various user permission levels in a project.
| Force push to protected branches [^2] | | | | | |
| Remove protected branches [^2] | | | | | |
-[^1]: If **Allow guest to access builds** is enabled in CI settings
+[^1]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines**
[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
## Group
@@ -138,3 +139,33 @@ instance and project. In addition, all admins can use the admin interface under
| Add shared runners | | | | ✓ |
| See events in the system | | | | ✓ |
| Admin interface | | | | ✓ |
+
+### Build permissions
+
+> Changed in GitLab 8.12.
+
+GitLab 8.12 has a completely redesigned build permissions system.
+Read all about the [new model and its implications][new-mod].
+
+This table shows granted privileges for builds triggered by specific types of
+users:
+
+| Action | Guest, Reporter | Developer | Master | Admin |
+|---------------------------------------------|-----------------|-------------|----------|--------|
+| Run CI build | | ✓ | ✓ | ✓ |
+| Clone source and LFS from current project | | ✓ | ✓ | ✓ |
+| Clone source and LFS from public projects | | ✓ | ✓ | ✓ |
+| Clone source and LFS from internal projects | | ✓ [^3] | ✓ [^3] | ✓ |
+| Clone source and LFS from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] |
+| Push source and LFS | | | | |
+| Pull container images from current project | | ✓ | ✓ | ✓ |
+| Pull container images from public projects | | ✓ | ✓ | ✓ |
+| Pull container images from internal projects| | ✓ [^3] | ✓ [^3] | ✓ |
+| Pull container images from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] |
+| Push container images to current project | | ✓ | ✓ | ✓ |
+| Push container images to other projects | | | | |
+
+[^3]: Only if user is not external one.
+[^4]: Only if user is a member of the project.
+[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
+[new-mod]: project/new_ci_build_permissions_model.md
diff --git a/doc/user/project/builds/artifacts.md b/doc/user/project/builds/artifacts.md
index c93ae1c369c..88f1863dddb 100644
--- a/doc/user/project/builds/artifacts.md
+++ b/doc/user/project/builds/artifacts.md
@@ -101,4 +101,36 @@ inside GitLab that make that possible.
![Build artifacts browser](img/build_artifacts_browser.png)
+## Downloading the latest build artifacts
+
+It is possible to download the latest artifacts of a build via a well known URL
+so you can use it for scripting purposes.
+
+The structure of the URL is the following:
+
+```
+https://example.com/<namespace>/<project>/builds/artifacts/<ref>/download?job=<job_name>
+```
+
+For example, to download the latest artifacts of the job named `rspec 6 20` of
+the `master` branch of the `gitlab-ce` project that belongs to the `gitlab-org`
+namespace, the URL would be:
+
+```
+https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=rspec+6+20
+```
+
+The latest builds are also exposed in the UI in various places. Specifically,
+look for the download button in:
+
+- the main project's page
+- the branches page
+- the tags page
+
+If the latest build has failed to upload the artifacts, you can see that
+information in the UI.
+
+![Latest artifacts button](img/build_latest_artifacts_browser.png)
+
+
[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
diff --git a/doc/user/project/builds/img/build_latest_artifacts_browser.png b/doc/user/project/builds/img/build_latest_artifacts_browser.png
new file mode 100644
index 00000000000..d8e9071958c
--- /dev/null
+++ b/doc/user/project/builds/img/build_latest_artifacts_browser.png
Binary files differ
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
new file mode 100644
index 00000000000..abef80e7914
--- /dev/null
+++ b/doc/user/project/cycle_analytics.md
@@ -0,0 +1,114 @@
+# Cycle Analytics
+
+> [Introduced][ce-5986] in GitLab 8.12.
+>
+> **Note:**
+This the first iteration of Cycle Analytics, you can follow the following issue
+to track the changes that are coming to this feature: [#20975][ce-20975].
+
+Cycle Analytics measures the time it takes to go from [an idea to production] for
+each project you have. This is achieved by not only indicating the total time it
+takes to reach at that point, but the total time is broken down into the
+multiple stages an idea has to pass through to be shipped.
+
+Cycle Analytics is that it is tightly coupled with the [GitLab flow] and
+calculates a separate median for each stage.
+
+## Overview
+
+You can find the Cycle Analytics page under your project's **Pipelines > Cycle
+Analytics** tab.
+
+![Cycle Analytics landing page](img/cycle_analytics_landing_page.png)
+
+You can see that there are seven stages in total:
+
+- **Issue** (Tracker)
+ - Median time from issue creation until given a milestone or list label
+ (first assignment, any milestone, milestone date or assignee is not required)
+- **Plan** (Board)
+ - Median time from giving an issue a milestone or label until pushing the
+ first commit
+- **Code** (IDE)
+ - Median time from the first commit until the merge request is created
+- **Test** (CI)
+ - Median total test time for all commits/merges
+- **Review** (Merge Request/MR)
+ - Median time from merge request creation until the merge request is merged
+ (closed merge requests won't be taken into account)
+- **Staging** (Continuous Deployment)
+ - Median time from when the merge request got merged until the deploy to
+ production (production is last stage/environment)
+- **Production** (Total)
+ - Sum of all the above stages excluding the Test (CI) time
+
+## How the data is measured
+
+Cycle Analytics records cycle time so only data on the issues that have been
+deployed to production are measured. In case you just started a new project and
+you have not pushed anything to production, then you will not be able to
+properly see the Cycle Analytics of your project.
+
+Specifically, if your CI is not set up and you have not defined a `production`
+[environment], then you will not have any data.
+
+Below you can see in more detail what the various stages of Cycle Analytics mean.
+
+| **Stage** | **Description** |
+| --------- | --------------- |
+| Issue | Measures the median time between creating an issue and taking action to solve it, by either labeling it or adding it to a milestone, whatever comes first. The label will be tracked only if it already has an [Issue Board list][board] created for it. |
+| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the repository. To make this change tracked, the pushed commit needs to contain the [issue closing pattern], for example `Closes #xxx`, where `xxx` is the number of the issue related to this commit. If the commit does not contain the issue closing pattern, it is not considered to the measurement time of the stage. |
+| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request related to that commit. The key to keep the process tracked is include the [issue closing pattern] to the description of the merge request. |
+| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. |
+| Review | Measures the median time taken to review the merge request, between its creation and until it's merged. |
+| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. |
+| Production| The sum of all time taken to run the entire process, from issue creation to deploying the code to production. |
+
+---
+
+Here's a little explanation of how this works behind the scenes:
+
+1. Issues and merge requests are grouped together in pairs, such that for each
+ `<issue, merge request>` pair, the merge request has `Fixes #xxx` for the
+ corresponding issue. All other issues and merge requests are **not** considered.
+
+1. Then the <issue, merge request> pairs are filtered out. Any merge request
+ that has **not** been deployed to production in the last XX days (specified
+ by the UI - default is 90 days) prohibits these pairs from being considered.
+
+1. For the remaining `<issue, merge request>` pairs, we check the information that
+ we need for the stages, like issue creation date, merge request merge time,
+ etc.
+
+To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all.
+So, if a merge request doesn't close an issue or an issue is not labeled with a
+label present in the Issue Board or assigned a milestone or a project has no
+`production` environment, the Cycle Analytics dashboard won't present any data
+at all.
+
+## Permissions
+
+The current permissions on the Cycle Analytics dashboard are:
+
+- Public projects - anyone can access
+- Private/internal projects - any member (guest level and above) can access
+
+You can [read more about permissions][permissions] in general.
+
+## More resources
+
+Learn more about Cycle Analytics in the following resources:
+
+- [Cycle Analytics feature page](https://about.gitlab.com/solutions/cycle-analytics/)
+- [Cycle Analytics feature preview](https://about.gitlab.com/2016/09/16/feature-preview-introducing-cycle-analytics/)
+- [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+
+
+[ce-5986]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5986
+[ce-20975]: https://gitlab.com/gitlab-org/gitlab-ce/issues/20975
+[GitLab flow]: ../../workflow/gitlab_flow.md
+[permissions]: ../permissions.md
+[environment]: ../../ci/yaml/README.md#environment
+[board]: issue_board.md#creating-a-new-list
+[idea to production]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab
+[issue closing pattern]: issues/automatic_issue_closing.md
diff --git a/doc/user/project/img/cycle_analytics_landing_page.png b/doc/user/project/img/cycle_analytics_landing_page.png
new file mode 100644
index 00000000000..4fa42c87395
--- /dev/null
+++ b/doc/user/project/img/cycle_analytics_landing_page.png
Binary files differ
diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md
new file mode 100644
index 00000000000..d6f3a7d5555
--- /dev/null
+++ b/doc/user/project/issues/automatic_issue_closing.md
@@ -0,0 +1,55 @@
+# Automatic issue closing
+
+>**Note:**
+This is the user docs. In order to change the default issue closing pattern,
+follow the steps in the [administration docs].
+
+When a commit or merge request resolves one or more issues, it is possible to
+automatically have these issues closed when the commit or merge request lands
+in the project's default branch.
+
+If a commit message or merge request description contains a sentence matching
+a certain regular expression, all issues referenced from the matched text will
+be closed. This happens when the commit is pushed to a project's **default**
+branch, or when a commit or merge request is merged into it.
+
+## Default closing pattern value
+
+When not specified, the default issue closing pattern as shown below will be
+used:
+
+```bash
+((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
+```
+
+Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's
+source code that can match a reference to 1) a local issue (`#123`),
+2) a cross-project issue (`group/project#123`) or 3) a link to an issue
+(`https://gitlab.example.com/group/project/issues/123`).
+
+---
+
+This translates to the following keywords:
+
+- Close, Closes, Closed, Closing, close, closes, closed, closing
+- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing
+- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving
+
+---
+
+For example the following commit message:
+
+```
+Awesome commit message
+
+Fix #20, Fixes #21 and Closes group/otherproject#22.
+This commit is also related to #17 and fixes #18, #19
+and https://gitlab.example.com/group/otherproject/issues/23.
+```
+
+will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed
+to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as
+it does not match the pattern. It works with multi-line commit messages as well
+as one-liners when used with `git commit -m`.
+
+[administration docs]: ../../../administration/issue_closing_pattern.md
diff --git a/doc/user/project/merge_requests.md b/doc/user/project/merge_requests.md
new file mode 100644
index 00000000000..5af9a5d049c
--- /dev/null
+++ b/doc/user/project/merge_requests.md
@@ -0,0 +1,169 @@
+# Merge Requests
+
+Merge requests allow you to exchange changes you made to source code and
+collaborate with other people on the same project.
+
+## Authorization for merge requests
+
+There are two main ways to have a merge request flow with GitLab:
+
+1. Working with [protected branches][] in a single repository
+1. Working with forks of an authoritative project
+
+[Learn more about the authorization for merge requests.](merge_requests/authorization_for_merge_requests.md)
+
+## Cherry-pick changes
+
+Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button
+in a merged merge requests or a commit.
+
+[Learn more about cherry-picking changes.](merge_requests/cherry_pick_changes.md)
+
+## Merge when build succeeds
+
+When reviewing a merge request that looks ready to merge but still has one or
+more CI builds running, you can set it to be merged automatically when all
+builds succeed. This way, you don't have to wait for the builds to finish and
+remember to merge the request manually.
+
+[Learn more about merging when build succeeds.](merge_requests/merge_when_build_succeeds.md)
+
+## Resolve discussion comments in merge requests reviews
+
+Keep track of the progress during a code review with resolving comments.
+Resolving comments prevents you from forgetting to address feedback and lets
+you hide discussions that are no longer relevant.
+
+[Read more about resolving discussion comments in merge requests reviews.](merge_requests/merge_request_discussion_resolution.md)
+
+## Resolve conflicts
+
+When a merge request has conflicts, GitLab may provide the option to resolve
+those conflicts in the GitLab UI.
+
+[Learn more about resolving merge conflicts in the UI.](merge_requests/resolve_conflicts.md)
+
+## Revert changes
+
+GitLab implements Git's powerful feature to revert any commit with introducing
+a **Revert** button in merge requests and commit details.
+
+[Learn more about reverting changes in the UI](merge_requests/revert_changes.md)
+
+## Merge requests versions
+
+Every time you push to a branch that is tied to a merge request, a new version
+of merge request diff is created. When you visit a merge request that contains
+more than one pushes, you can select and compare the versions of those merge
+request diffs.
+
+[Read more about the merge requests versions.](merge_requests/versions.md)
+
+## Work In Progress merge requests
+
+To prevent merge requests from accidentally being accepted before they're
+completely ready, GitLab blocks the "Accept" button for merge requests that
+have been marked as a **Work In Progress**.
+
+[Learn more about settings a merge request as "Work In Progress".](merge_requests/work_in_progress_merge_requests.md)
+
+## Ignore whitespace changes in Merge Request diff view
+
+If you click the **Hide whitespace changes** button, you can see the diff
+without whitespace changes (if there are any). This is also working when on a
+specific commit page.
+
+![MR diff](merge_requests/img/merge_request_diff.png)
+
+>**Tip:**
+You can append `?w=1` while on the diffs page of a merge request to ignore any
+whitespace changes.
+
+## Tips
+
+Here are some tips that will help you be more efficient with merge requests in
+the command line.
+
+> **Note:**
+This section might move in its own document in the future.
+
+### Checkout merge requests locally
+
+A merge request contains all the history from a repository, plus the additional
+commits added to the branch associated with the merge request. Here's a few
+tricks to checkout a merge request locally.
+
+Please note that you can checkout a merge request locally even if the source
+project is a fork (even a private fork) of the target project.
+
+#### Checkout locally by adding a git alias
+
+Add the following alias to your `~/.gitconfig`:
+
+```
+[alias]
+ mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
+```
+
+Now you can check out a particular merge request from any repository and any
+remote. For example, to check out the merge request with ID 5 as shown in GitLab
+from the `upstream` remote, do:
+
+```
+git mr upstream 5
+```
+
+This will fetch the merge request into a local `mr-upstream-5` branch and check
+it out.
+
+#### Checkout locally by modifying `.git/config` for a given repository
+
+Locate the section for your GitLab remote in the `.git/config` file. It looks
+like this:
+
+```
+[remote "origin"]
+ url = https://gitlab.com/gitlab-org/gitlab-ce.git
+ fetch = +refs/heads/*:refs/remotes/origin/*
+```
+
+You can open the file with:
+
+```
+git config -e
+```
+
+Now add the following line to the above section:
+
+```
+fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
+```
+
+In the end, it should look like this:
+
+```
+[remote "origin"]
+ url = https://gitlab.com/gitlab-org/gitlab-ce.git
+ fetch = +refs/heads/*:refs/remotes/origin/*
+ fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
+```
+
+Now you can fetch all the merge requests:
+
+```
+git fetch origin
+
+...
+From https://gitlab.com/gitlab-org/gitlab-ce.git
+ * [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
+ * [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
+...
+```
+
+And to check out a particular merge request:
+
+```
+git checkout origin/merge-requests/1
+```
+
+[protected branches]: protected_branches.md
diff --git a/doc/user/project/merge_requests/authorization_for_merge_requests.md b/doc/user/project/merge_requests/authorization_for_merge_requests.md
new file mode 100644
index 00000000000..59b3fe7242c
--- /dev/null
+++ b/doc/user/project/merge_requests/authorization_for_merge_requests.md
@@ -0,0 +1,56 @@
+# Authorization for Merge requests
+
+There are two main ways to have a merge request flow with GitLab:
+
+1. Working with [protected branches] in a single repository.
+1. Working with forks of an authoritative project.
+
+## Protected branch flow
+
+With the protected branch flow everybody works within the same GitLab project.
+
+The project maintainers get Master access and the regular developers get
+Developer access.
+
+The maintainers mark the authoritative branches as 'Protected'.
+
+The developers push feature branches to the project and create merge requests
+to have their feature branches reviewed and merged into one of the protected
+branches.
+
+By default, only users with Master access can merge changes into a protected
+branch.
+
+**Advantages**
+
+- Fewer projects means less clutter.
+- Developers need to consider only one remote repository.
+
+**Disadvantages**
+
+- Manual setup of protected branch required for each new project
+
+## Forking workflow
+
+With the forking workflow the maintainers get Master access and the regular
+developers get Reporter access to the authoritative repository, which prohibits
+them from pushing any changes to it.
+
+Developers create forks of the authoritative project and push their feature
+branches to their own forks.
+
+To get their changes into master they need to create a merge request across
+forks.
+
+**Advantages**
+
+- In an appropriately configured GitLab group, new projects automatically get
+ the required access restrictions for regular developers: fewer manual steps
+ to configure authorization for new projects.
+
+**Disadvantages**
+
+- The project need to keep their forks up to date, which requires more advanced
+ Git skills (managing multiple remotes).
+
+[protected branches]: ../protected_branches.md
diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md
new file mode 100644
index 00000000000..64b94d81024
--- /dev/null
+++ b/doc/user/project/merge_requests/cherry_pick_changes.md
@@ -0,0 +1,52 @@
+# Cherry-pick changes
+
+> [Introduced][ce-3514] in GitLab 8.7.
+
+---
+
+GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick]
+with introducing a **Cherry-pick** button in Merge Requests and commit details.
+
+## Cherry-picking a Merge Request
+
+After the Merge Request has been merged, a **Cherry-pick** button will be available
+to cherry-pick the changes introduced by that Merge Request:
+
+![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png)
+
+---
+
+You can cherry-pick the changes directly into the selected branch or you can opt to
+create a new Merge Request with the cherry-pick changes:
+
+![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png)
+
+## Cherry-picking a Commit
+
+You can cherry-pick a Commit from the Commit details page:
+
+![Cherry-pick commit](img/cherry_pick_changes_commit.png)
+
+---
+
+Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes
+directly into the target branch or create a new Merge Request to cherry-pick the
+changes:
+
+![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png)
+
+---
+
+Please note that when cherry-picking merge commits, the mainline will always be the
+first parent. If you want to use a different mainline then you need to do that
+from the command line.
+
+Here is a quick example to cherry-pick a merge commit using the second parent as the
+mainline:
+
+```bash
+git cherry-pick -m 2 7a39eb0
+```
+
+[ce-3514]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3514 "Cherry-pick button Merge Request"
+[git-cherry-pick]: https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation"
diff --git a/doc/workflow/img/cherry_pick_changes_commit.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png
index 7fb68cc9e9b..7fb68cc9e9b 100644
--- a/doc/workflow/img/cherry_pick_changes_commit.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_commit_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png
index 5267e04562f..5267e04562f 100644
--- a/doc/workflow/img/cherry_pick_changes_commit_modal.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_mr.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png
index 975fb13e463..975fb13e463 100644
--- a/doc/workflow/img/cherry_pick_changes_mr.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_mr_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png
index 6c003bacbe3..6c003bacbe3 100644
--- a/doc/workflow/img/cherry_pick_changes_mr_modal.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png
Binary files differ
diff --git a/doc/workflow/merge_requests/commit_compare.png b/doc/user/project/merge_requests/img/commit_compare.png
index 0e4a2b23c04..0e4a2b23c04 100644
--- a/doc/workflow/merge_requests/commit_compare.png
+++ b/doc/user/project/merge_requests/img/commit_compare.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_request_diff.png b/doc/user/project/merge_requests/img/merge_request_diff.png
new file mode 100644
index 00000000000..06ee4908edc
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_request_diff.png
Binary files differ
diff --git a/doc/workflow/merge_when_build_succeeds/enable.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png
index b86e6d7b3fd..b86e6d7b3fd 100644
--- a/doc/workflow/merge_when_build_succeeds/enable.png
+++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png
new file mode 100644
index 00000000000..6b9756b7418
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png
Binary files differ
diff --git a/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png
index 18bebf5fe92..18bebf5fe92 100644
--- a/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png
+++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png
Binary files differ
diff --git a/doc/workflow/merge_when_build_succeeds/status.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png
index f3ea61d8147..f3ea61d8147 100644
--- a/doc/workflow/merge_when_build_succeeds/status.png
+++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_commit.png b/doc/user/project/merge_requests/img/revert_changes_commit.png
index e7194fc3504..e7194fc3504 100644
--- a/doc/workflow/img/revert_changes_commit.png
+++ b/doc/user/project/merge_requests/img/revert_changes_commit.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_commit_modal.png b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png
index c660ec7eaec..c660ec7eaec 100644
--- a/doc/workflow/img/revert_changes_commit_modal.png
+++ b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_mr.png b/doc/user/project/merge_requests/img/revert_changes_mr.png
index 3002f0ac1c5..3002f0ac1c5 100644
--- a/doc/workflow/img/revert_changes_mr.png
+++ b/doc/user/project/merge_requests/img/revert_changes_mr.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_mr_modal.png b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png
index c6aaeecc8a6..c6aaeecc8a6 100644
--- a/doc/workflow/img/revert_changes_mr_modal.png
+++ b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions-compare.png b/doc/user/project/merge_requests/img/versions-compare.png
new file mode 100644
index 00000000000..890cae7768c
--- /dev/null
+++ b/doc/user/project/merge_requests/img/versions-compare.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions-dropdown.png b/doc/user/project/merge_requests/img/versions-dropdown.png
new file mode 100644
index 00000000000..9bab9304e14
--- /dev/null
+++ b/doc/user/project/merge_requests/img/versions-dropdown.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions.png b/doc/user/project/merge_requests/img/versions.png
new file mode 100644
index 00000000000..6c86f2c68ac
--- /dev/null
+++ b/doc/user/project/merge_requests/img/versions.png
Binary files differ
diff --git a/doc/workflow/wip_merge_requests/blocked_accept_button.png b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png
index 89c458aa8d9..89c458aa8d9 100644
--- a/doc/workflow/wip_merge_requests/blocked_accept_button.png
+++ b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png
Binary files differ
diff --git a/doc/workflow/wip_merge_requests/mark_as_wip.png b/doc/user/project/merge_requests/img/wip_mark_as_wip.png
index 9c37354a653..9c37354a653 100644
--- a/doc/workflow/wip_merge_requests/mark_as_wip.png
+++ b/doc/user/project/merge_requests/img/wip_mark_as_wip.png
Binary files differ
diff --git a/doc/workflow/wip_merge_requests/unmark_as_wip.png b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png
index 31f7326beb0..31f7326beb0 100644
--- a/doc/workflow/wip_merge_requests/unmark_as_wip.png
+++ b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png
Binary files differ
diff --git a/doc/user/project/merge_requests/merge_when_build_succeeds.md b/doc/user/project/merge_requests/merge_when_build_succeeds.md
new file mode 100644
index 00000000000..011f9cbc381
--- /dev/null
+++ b/doc/user/project/merge_requests/merge_when_build_succeeds.md
@@ -0,0 +1,46 @@
+# Merge When Build Succeeds
+
+When reviewing a merge request that looks ready to merge but still has one or
+more CI builds running, you can set it to be merged automatically when all
+builds succeed. This way, you don't have to wait for the builds to finish and
+remember to merge the request manually.
+
+![Enable](img/merge_when_build_succeeds_enable.png)
+
+When you hit the "Merge When Build Succeeds" button, the status of the merge
+request will be updated to represent the impending merge. If you cannot wait
+for the build to succeed and want to merge immediately, this option is available
+in the dropdown menu on the right of the main button.
+
+Both team developers and the author of the merge request have the option to
+cancel the automatic merge if they find a reason why it shouldn't be merged
+after all.
+
+![Status](img/merge_when_build_succeeds_status.png)
+
+When the build succeeds, the merge request will automatically be merged. When
+the build fails, the author gets a chance to retry any failed builds, or to
+push new commits to fix the failure.
+
+When the builds are retried and succeed on the second try, the merge request
+will automatically be merged after all. When the merge request is updated with
+new commits, the automatic merge is automatically canceled to allow the new
+changes to be reviewed.
+
+## Only allow merge requests to be merged if the build succeeds
+
+> **Note:**
+You need to have builds configured to enable this feature.
+
+You can prevent merge requests from being merged if their build did not succeed.
+
+Navigate to your project's settings page, select the
+**Only allow merge requests to be merged if the build succeeds** check box and
+hit **Save** for the changes to take effect.
+
+![Only allow merge if build succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png)
+
+From now on, every time the build fails you will not be able to merge the merge
+request from the UI, until you make the build pass.
+
+![Only allow merge if build succeeds msg](img/merge_when_build_succeeds_only_if_succeeds_msg.png)
diff --git a/doc/user/project/merge_requests/revert_changes.md b/doc/user/project/merge_requests/revert_changes.md
new file mode 100644
index 00000000000..5ead9f4177f
--- /dev/null
+++ b/doc/user/project/merge_requests/revert_changes.md
@@ -0,0 +1,64 @@
+# Reverting changes
+
+> [Introduced][ce-1990] in GitLab 8.5.
+
+---
+
+GitLab implements Git's powerful feature to [revert any commit][git-revert]
+with introducing a **Revert** button in Merge Requests and commit details.
+
+## Reverting a Merge Request
+
+_**Note:** The **Revert** button will only be available for Merge Requests
+created since GitLab 8.5. However, you can still revert a Merge Request
+by reverting the merge commit from the list of Commits page._
+
+After the Merge Request has been merged, a **Revert** button will be available
+to revert the changes introduced by that Merge Request:
+
+![Revert Merge Request](img/revert_changes_mr.png)
+
+---
+
+You can revert the changes directly into the selected branch or you can opt to
+create a new Merge Request with the revert changes:
+
+![Revert Merge Request modal](img/revert_changes_mr_modal.png)
+
+---
+
+After the Merge Request has been reverted, the **Revert** button will not be
+available anymore.
+
+## Reverting a Commit
+
+You can revert a Commit from the Commit details page:
+
+![Revert commit](img/revert_changes_commit.png)
+
+---
+
+Similar to reverting a Merge Request, you can opt to revert the changes
+directly into the target branch or create a new Merge Request to revert the
+changes:
+
+![Revert commit modal](img/revert_changes_commit_modal.png)
+
+---
+
+After the Commit has been reverted, the **Revert** button will not be available
+anymore.
+
+Please note that when reverting merge commits, the mainline will always be the
+first parent. If you want to use a different mainline then you need to do that
+from the command line.
+
+Here is a quick example to revert a merge commit using the second parent as the
+mainline:
+
+```bash
+git revert -m 2 7a39eb0
+```
+
+[ce-1990]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1990 "Revert button Merge Request"
+[git-revert]: https://git-scm.com/docs/git-revert "Git revert documentation"
diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md
new file mode 100644
index 00000000000..2805fdf635c
--- /dev/null
+++ b/doc/user/project/merge_requests/versions.md
@@ -0,0 +1,32 @@
+# Merge requests versions
+
+> Will be [introduced][ce-5467] in GitLab 8.12.
+
+Every time you push to a branch that is tied to a merge request, a new version
+of merge request diff is created. When you visit a merge request that contains
+more than one pushes, you can select and compare the versions of those merge
+request diffs.
+
+![Merge Request Versions](img/versions.png)
+
+By default, the latest version of changes is shown. However, you
+can select an older one from version dropdown.
+
+![Merge Request Versions](img/versions-dropdown.png)
+
+You can also compare the merge request version with older one to see what is
+changed since then.
+
+![Merge Request Versions](img/versions-compare.png)
+
+Please note that comments are disabled while viewing outdated merge versions
+or comparing to versions other than base.
+
+---
+
+>**Note:**
+Merge request versions are based on push not on commit. So, if you pushed 5
+commits in a single push, it will be a single option in the dropdown. If you
+pushed 5 times, that will count for 5 options.
+
+[ce-5467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5467
diff --git a/doc/user/project/merge_requests/work_in_progress_merge_requests.md b/doc/user/project/merge_requests/work_in_progress_merge_requests.md
new file mode 100644
index 00000000000..546c8bdc5e5
--- /dev/null
+++ b/doc/user/project/merge_requests/work_in_progress_merge_requests.md
@@ -0,0 +1,17 @@
+# "Work In Progress" Merge Requests
+
+To prevent merge requests from accidentally being accepted before they're
+completely ready, GitLab blocks the "Accept" button for merge requests that
+have been marked a **Work In Progress**.
+
+![Blocked Accept Button](img/wip_blocked_accept_button.png)
+
+To mark a merge request a Work In Progress, simply start its title with `[WIP]`
+or `WIP:`.
+
+![Mark as WIP](img/wip_mark_as_wip.png)
+
+To allow a Work In Progress merge request to be accepted again when it's ready,
+simply remove the `WIP` prefix.
+
+![Unark as WIP](img/wip_unmark_as_wip.png)
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
new file mode 100644
index 00000000000..e73f60023b5
--- /dev/null
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -0,0 +1,289 @@
+# New CI build permissions model
+
+> Introduced in GitLab 8.12.
+
+GitLab 8.12 has a completely redesigned [build permissions] system. You can find
+all discussion and all our concerns when choosing the current approach in issue
+[#18994](https://gitlab.com/gitlab-org/gitlab-ce/issues/18994).
+
+---
+
+Builds permissions should be tightly integrated with the permissions of a user
+who is triggering a build.
+
+The reasons to do it like that are:
+
+- We already have a permissions system in place: group and project membership
+ of users.
+- We already fully know who is triggering a build (using `git push`, using the
+ web UI, executing triggers).
+- We already know what user is allowed to do.
+- We use the user permissions for builds that are triggered by the user.
+- It opens a lot of possibilities to further enforce user permissions, like
+ allowing only specific users to access runners or use secure variables and
+ environments.
+- It is simple and convenient that your build can access everything that you
+ as a user have access to.
+- Short living unique tokens are now used, granting access for time of the build
+ and maximizing security.
+
+With the new behavior, any build that is triggered by the user, is also marked
+with their permissions. When a user does a `git push` or changes files through
+the web UI, a new pipeline will be usually created. This pipeline will be marked
+as created be the pusher (local push or via the UI) and any build created in this
+pipeline will have the permissions of the pusher.
+
+This allows us to make it really easy to evaluate the access for all projects
+that have Git submodules or are using container images that the pusher would
+have access too. **The permission is granted only for time that build is running.
+The access is revoked after the build is finished.**
+
+## Types of users
+
+It is important to note that we have a few types of users:
+
+- **Administrators**: CI builds created by Administrators will not have access
+ to all GitLab projects, but only to projects and container images of projects
+ that the administrator is a member of.That means that if a project is either
+ public or internal users have access anyway, but if a project is private, the
+ Administrator will have to be a member of it in order to have access to it
+ via another project's build.
+
+- **External users**: CI builds created by [external users][ext] will have
+ access only to projects to which user has at least reporter access. This
+ rules out accessing all internal projects by default,
+
+This allows us to make the CI and permission system more trustworthy.
+Let's consider the following scenario:
+
+1. You are an employee of a company. Your company has a number of internal tools
+ hosted in private repositories and you have multiple CI builds that make use
+ of these repositories.
+
+2. You invite a new [external user][ext]. CI builds created by that user do not
+ have access to internal repositories, because the user also doesn't have the
+ access from within GitLab. You as an employee have to grant explicit access
+ for this user. This allows us to prevent from accidental data leakage.
+
+## Build token
+
+A unique build token is generated for each build and it allows the user to
+access all projects that would be normally accessible to the user creating that
+build.
+
+We try to make sure that this token doesn't leak by:
+
+1. Securing all API endpoints to not expose the build token.
+1. Masking the build token from build logs.
+1. Allowing to use the build token **only** when build is running.
+
+However, this brings a question about the Runners security. To make sure that
+this token doesn't leak, you should also make sure that you configure
+your Runners in the most possible secure way, by avoiding the following:
+
+1. Any usage of Docker's `privileged` mode is risky if the machines are re-used.
+1. Using the `shell` executor since builds run on the same machine.
+
+By using an insecure GitLab Runner configuration, you allow the rogue developers
+to steal the tokens of other builds.
+
+## Debugging problems
+
+With the new permission model in place, there may be times that your build will
+fail. This is most likely because your project tries to access other project's
+sources, and you don't have the appropriate permissions. In the build log look
+for information about 403 or forbidden access messages
+
+As an Administrator, you can verify that the user is a member of the group or
+project they're trying to have access to, and you can impersonate the user to
+retry the failing build in order to verify that everything is correct.
+
+## Build triggers
+
+[Build triggers][triggers] do not support the new permission model.
+They continue to use the old authentication mechanism where the CI build
+can access only its own sources. We plan to remove that limitation in one of
+the upcoming releases.
+
+## Before GitLab 8.12
+
+In versions before GitLab 8.12, all CI builds would use the CI Runner's token
+to checkout project sources.
+
+The project's Runner's token was a token that you could find under the
+project's **Settings > CI/CD Pipelines** and was limited to access only that
+project.
+It could be used for registering new specific Runners assigned to the project
+and to checkout project sources.
+It could also be used with the GitLab Container Registry for that project,
+allowing pulling and pushing Docker images from within the CI build.
+
+---
+
+GitLab would create a special checkout URL like:
+
+```
+https://gitlab-ci-token:<project-runners-token>/gitlab.com/gitlab-org/gitlab-ce.git
+```
+
+And then the users could also use it in their CI builds all Docker related
+commands to interact with GitLab Container Registry. For example:
+
+```
+docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com
+```
+
+Using single token had multiple security implications:
+
+- The token would be readable to anyone who had developer access to a project
+ that could run CI builds, allowing the developer to register any specific
+ Runner for that project.
+- The token would allow to access only the project's sources, forbidding from
+ accessing any other projects.
+- The token was not expiring and was multi-purpose: used for checking out sources,
+ for registering specific runners and for accessing a project's container
+ registry with read-write permissions.
+
+All the above led to a new permission model for builds that was introduced
+with GitLab 8.12.
+
+## Making use of the new CI build permissions model
+
+With the new build permission model, there is now an easy way to access all
+dependent source code in a project. That way, we can:
+
+1. Access a project's Git submodules
+1. Access private container images
+1. Access project's and submodule LFS objects
+
+Let's see how that works with Git submodules and private Docker images hosted on
+the container registry.
+
+## Git submodules
+
+>
+It often happens that while working on one project, you need to use another
+project from within it; perhaps it’s a library that a third party developed or
+you’re developing a project separately and are using it in multiple parent
+projects.
+A common issue arises in these scenarios: you want to be able to treat the two
+projects as separate yet still be able to use one from within the other.
+>
+_Excerpt from the [Git website][git-scm] about submodules._
+
+If dealing with submodules, your project will probably have a file named
+`.gitmodules`. And this is how it usually looks like:
+
+```
+[submodule "tools"]
+ path = tools
+ url = git@gitlab.com/group/tools.git
+```
+
+> **Note:**
+If you are **not** using GitLab 8.12 or higher, you would need to work your way
+around this issue in order to access the sources of `gitlab.com/group/tools`
+(e.g., use [SSH keys](../ssh_keys/README.md)).
+>
+With GitLab 8.12 onward, your permissions are used to evaluate what a CI build
+can access. More information about how this system works can be found in the
+[Build permissions model](../../user/permissions.md#builds-permissions).
+
+To make use of the new changes, you have to update your `.gitmodules` file to
+use a relative URL.
+
+Let's consider the following example:
+
+1. Your project is located at `https://gitlab.com/secret-group/my-project`.
+1. To checkout your sources you usually use an SSH address like
+ `git@gitlab.com:secret-group/my-project.git`.
+1. Your project depends on `https://gitlab.com/group/tools`.
+1. You have the `.gitmodules` file with above content.
+
+Since Git allows the usage of relative URLs for your `.gitmodules` configuration,
+this easily allows you to use HTTP for cloning all your CI builds and SSH
+for all your local checkouts.
+
+For example, if you change the `url` of your `tools` dependency, from
+`git@gitlab.com/group/tools.git` to `../../group/tools.git`, this will instruct
+Git to automatically deduce the URL that should be used when cloning sources.
+Whether you use HTTP or SSH, Git will use that same channel and it will allow
+to make all your CI builds use HTTPS (because GitLab CI uses HTTPS for cloning
+your sources), and all your local clones will continue using SSH.
+
+Given the above explanation, your `.gitmodules` file should eventually look
+like this:
+
+```
+[submodule "tools"]
+ path = tools
+ url = ../../group/tools.git
+```
+
+However, you have to explicitly tell GitLab CI to clone your submodules as this
+is not done automatically. You can achieve that by adding a `before_script`
+section to your `.gitlab-ci.yml`:
+
+```
+before_script:
+ - git submodule update --init --recursive
+
+test:
+ script:
+ - run-my-tests
+```
+
+This will make GitLab CI initialize (fetch) and update (checkout) all your
+submodules recursively.
+
+In case your environment or your Docker image doesn't have Git installed,
+you have to either ask your Administrator or install the missing dependency
+yourself:
+
+```
+# Debian / Ubuntu
+before_script:
+ - apt-get update -y
+ - apt-get install -y git-core
+ - git submodule update --init --recursive
+
+# CentOS / RedHat
+before_script:
+ - yum install git
+ - git submodule update --init --recursive
+
+# Alpine
+before_script:
+ - apk add -U git
+ - git submodule update --init --recursive
+```
+
+### Container Registry
+
+With the update permission model we also extended the support for accessing
+Container Registries for private projects.
+
+> **Note:**
+As GitLab Runner 1.6 doesn't yet incorporate the introduced changes for
+permissions, this makes the `image:` directive to not work with private projects
+automatically. The manual configuration by an Administrator is required to use
+private images. We plan to remove that limitation in one of the upcoming releases.
+
+Your builds can access all container images that you would normally have access
+to. The only implication is that you can push to the Container Registry of the
+project for which the build is triggered.
+
+This is how an example usage can look like:
+
+```
+test:
+ script:
+ - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+ - docker pull $CI_REGISTRY/group/other-project:latest
+ - docker run $CI_REGISTRY/group/other-project:latest
+```
+
+[git-scm]: https://git-scm.com/book/en/v2/Git-Tools-Submodules
+[build permissions]: ../permissions.md#builds-permissions
+[ext]: ../permissions.md#external-users
+[triggers]: ../../ci/triggers/README.md
diff --git a/doc/workflow/img/web_editor_new_branch_dropdown.png b/doc/user/project/repository/img/web_editor_new_branch_dropdown.png
index a8e635d2faf..a8e635d2faf 100644
--- a/doc/workflow/img/web_editor_new_branch_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_new_branch_dropdown.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_branch_page.png b/doc/user/project/repository/img/web_editor_new_branch_page.png
index 7f36b7faf63..7f36b7faf63 100644
--- a/doc/workflow/img/web_editor_new_branch_page.png
+++ b/doc/user/project/repository/img/web_editor_new_branch_page.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_directory_dialog.png b/doc/user/project/repository/img/web_editor_new_directory_dialog.png
index d16e3c67116..d16e3c67116 100644
--- a/doc/workflow/img/web_editor_new_directory_dialog.png
+++ b/doc/user/project/repository/img/web_editor_new_directory_dialog.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_directory_dropdown.png b/doc/user/project/repository/img/web_editor_new_directory_dropdown.png
index c8d77b16ee8..c8d77b16ee8 100644
--- a/doc/workflow/img/web_editor_new_directory_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_new_directory_dropdown.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_file_dropdown.png b/doc/user/project/repository/img/web_editor_new_file_dropdown.png
index 3fcb91c9b93..3fcb91c9b93 100644
--- a/doc/workflow/img/web_editor_new_file_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_new_file_dropdown.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_file_editor.png b/doc/user/project/repository/img/web_editor_new_file_editor.png
index 21c340b9288..21c340b9288 100644
--- a/doc/workflow/img/web_editor_new_file_editor.png
+++ b/doc/user/project/repository/img/web_editor_new_file_editor.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_push_widget.png b/doc/user/project/repository/img/web_editor_new_push_widget.png
index c7738a4c930..c7738a4c930 100644
--- a/doc/workflow/img/web_editor_new_push_widget.png
+++ b/doc/user/project/repository/img/web_editor_new_push_widget.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_tag_dropdown.png b/doc/user/project/repository/img/web_editor_new_tag_dropdown.png
index ac7415009b3..ac7415009b3 100644
--- a/doc/workflow/img/web_editor_new_tag_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_new_tag_dropdown.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_tag_page.png b/doc/user/project/repository/img/web_editor_new_tag_page.png
index 231e1a13fc0..231e1a13fc0 100644
--- a/doc/workflow/img/web_editor_new_tag_page.png
+++ b/doc/user/project/repository/img/web_editor_new_tag_page.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_start_new_merge_request.png b/doc/user/project/repository/img/web_editor_start_new_merge_request.png
index 2755501dfd1..2755501dfd1 100644
--- a/doc/workflow/img/web_editor_start_new_merge_request.png
+++ b/doc/user/project/repository/img/web_editor_start_new_merge_request.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png b/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png
new file mode 100644
index 00000000000..4efc51cc423
--- /dev/null
+++ b/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png b/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png
new file mode 100644
index 00000000000..67190c58823
--- /dev/null
+++ b/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png b/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png
new file mode 100644
index 00000000000..47719113805
--- /dev/null
+++ b/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_upload_file_dialog.png b/doc/user/project/repository/img/web_editor_upload_file_dialog.png
index 9d6d8250bbe..9d6d8250bbe 100644
--- a/doc/workflow/img/web_editor_upload_file_dialog.png
+++ b/doc/user/project/repository/img/web_editor_upload_file_dialog.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_upload_file_dropdown.png b/doc/user/project/repository/img/web_editor_upload_file_dropdown.png
index 6b5205b05ec..6b5205b05ec 100644
--- a/doc/workflow/img/web_editor_upload_file_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_upload_file_dropdown.png
Binary files differ
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
new file mode 100644
index 00000000000..993c6bfb7e9
--- /dev/null
+++ b/doc/user/project/repository/web_editor.md
@@ -0,0 +1,175 @@
+# GitLab Web Editor
+
+Sometimes it's easier to make quick changes directly from the GitLab interface
+than to clone the project and use the Git command line tool. In this feature
+highlight we look at how you can create a new file, directory, branch or
+tag from the file browser. All of these actions are available from a single
+dropdown menu.
+
+## Create a file
+
+From a project's files page, click the '+' button to the right of the branch selector.
+Choose **New file** from the dropdown.
+
+![New file dropdown menu](img/web_editor_new_file_dropdown.png)
+
+---
+
+Enter a file name in the **File name** box. Then, add file content in the editor
+area. Add a descriptive commit message and choose a branch. The branch field
+will default to the branch you were viewing in the file browser. If you enter
+a new branch name, a checkbox will appear allowing you to start a new merge
+request after you commit the changes.
+
+When you are satisfied with your new file, click **Commit Changes** at the bottom.
+
+![Create file editor](img/web_editor_new_file_editor.png)
+
+### Template dropdowns
+
+When starting a new project, there are some common files which the new project
+might need too. Therefore a message will be displayed by GitLab to make this
+easy for you.
+
+![First file for your project](img/web_editor_template_dropdown_first_file.png)
+
+When clicking on either `LICENSE` or `.gitignore`, a dropdown will be displayed
+to provide you with a template which might be suitable for your project.
+
+![MIT license selected](img/web_editor_template_dropdown_mit_license.png)
+
+The license, changelog, contribution guide, or `.gitlab-ci.yml` file could also
+be added through a button on the project page. In the example below the license
+has already been created, which creates a link to the license itself.
+
+![New file button](img/web_editor_template_dropdown_buttons.png)
+
+>**Note:**
+The **Set up CI** button will not appear on an empty repository. You have to at
+least add a file in order for the button to show up.
+
+## Upload a file
+
+The ability to create a file is great when the content is text. However, this
+doesn't work well for binary data such as images, PDFs or other file types. In
+this case you need to upload a file.
+
+From a project's files page, click the '+' button to the right of the branch
+selector. Choose **Upload file** from the dropdown.
+
+![Upload file dropdown menu](img/web_editor_upload_file_dropdown.png)
+
+---
+
+Once the upload dialog pops up there are two ways to upload your file. Either
+drag and drop a file on the pop up or use the **click to upload** link. A file
+preview will appear once you have selected a file to upload.
+
+Enter a commit message, choose a branch, and click **Upload file** when you are
+ready.
+
+![Upload file dialog](img/web_editor_upload_file_dialog.png)
+
+## Create a directory
+
+To keep files in the repository organized it is often helpful to create a new
+directory.
+
+From a project's files page, click the '+' button to the right of the branch selector.
+Choose **New directory** from the dropdown.
+
+![New directory dropdown](img/web_editor_new_directory_dropdown.png)
+
+---
+
+In the new directory dialog enter a directory name, a commit message and choose
+the target branch. Click **Create directory** to finish.
+
+![New directory dialog](img/web_editor_new_directory_dialog.png)
+
+## Create a new branch
+
+There are multiple ways to create a branch from GitLab's web interface.
+
+### Create a new branch from an issue
+
+> [Introduced][ce-2808] in GitLab 8.6.
+
+In case your development workflow dictates to have an issue for every merge
+request, you can quickly create a branch right on the issue page which will be
+tied with the issue itself. You can see a **New Branch** button after the issue
+description, unless there is already a branch with the same name or a referenced
+merge request.
+
+![New Branch Button](img/new_branch_from_issue.png)
+
+Once you click it, a new branch will be created that diverges from the default
+branch of your project, by default `master`. The branch name will be based on
+the title of the issue and as suffix it will have its ID. Thus, the example
+screenshot above will yield a branch named
+`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`.
+
+After the branch is created, you can edit files in the repository to fix
+the issue. When a merge request is created based on the newly created branch,
+the description field will automatically display the [issue closing pattern]
+`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the
+merge request is merged.
+
+### Create a new branch from a project's dashboard
+
+If you want to make changes to several files before creating a new merge
+request, you can create a new branch up front. From a project's files page,
+choose **New branch** from the dropdown.
+
+![New branch dropdown](img/web_editor_new_branch_dropdown.png)
+
+---
+
+Enter a new **Branch name**. Optionally, change the **Create from** field
+to choose which branch, tag or commit SHA this new branch will originate from.
+This field will autocomplete if you start typing an existing branch or tag.
+Click **Create branch** and you will be returned to the file browser on this new
+branch.
+
+![New branch page](img/web_editor_new_branch_page.png)
+
+---
+
+You can now make changes to any files, as needed. When you're ready to merge
+the changes back to master you can use the widget at the top of the screen.
+This widget only appears for a period of time after you create the branch or
+modify files.
+
+![New push widget](img/web_editor_new_push_widget.png)
+
+## Create a new tag
+
+Tags are useful for marking major milestones such as production releases,
+release candidates, and more. You can create a tag from a branch or a commit
+SHA. From a project's files page, choose **New tag** from the dropdown.
+
+![New tag dropdown](img/web_editor_new_tag_dropdown.png)
+
+---
+
+Give the tag a name such as `v1.0.0`. Choose the branch or SHA from which you
+would like to create this new tag. You can optionally add a message and
+release notes. The release notes section supports markdown format and you can
+also upload an attachment. Click **Create tag** and you will be taken to the tag
+list page.
+
+![New tag page](img/web_editor_new_tag_page.png)
+
+## Tips
+
+When creating or uploading a new file, or creating a new directory, you can
+trigger a new merge request rather than committing directly to master. Enter
+a new branch name in the **Target branch** field. You will notice a checkbox
+appear that is labeled **Start a new merge request with these changes**. After
+you commit the changes you will be taken to a new merge request form.
+
+![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png)
+
+![New file button](basicsimages/file_button.png)
+[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
+[issue closing pattern]: ../user/project/issues/automatic_issue_closing.md
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 08ff89ce6ae..445c0ee8333 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -3,8 +3,8 @@
>**Notes:**
>
> - [Introduced][ce-3050] in GitLab 8.9.
-> - Importing will not be possible if the import instance version is lower
-> than that of the exporter.
+> - Importing will not be possible if the import instance version differs from
+> that of the exporter.
> - For existing installations, the project import option has to be enabled in
> application settings (`/admin/application_settings`) under 'Import sources'.
> You will have to be an administrator to enable and use the import functionality.
@@ -17,6 +17,20 @@
Existing projects running on any GitLab instance or GitLab.com can be exported
with all their related data and be moved into a new GitLab instance.
+## Version history
+
+| GitLab version | Import/Export version |
+| -------- | -------- |
+| 8.12.0 to current | 0.1.4 |
+| 8.10.3 | 0.1.3 |
+| 8.10.0 | 0.1.2 |
+| 8.9.5 | 0.1.1 |
+| 8.9.0 | 0.1.0 |
+
+ > The table reflects what GitLab version we updated the Import/Export version at.
+ > For instance, 8.10.3 and 8.11 will have the same Import/Export version (0.1.3)
+ > and the exports between them will be compatible.
+
## Exported contents
The following items will be exported:
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 0cf56449de2..2d9bfbc0629 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -1,7 +1,8 @@
# Workflow
-- [Authorization for merge requests](authorization_for_merge_requests.md)
+- [Automatic issue closing](../user/project/issues/automatic_issue_closing.md)
- [Change your time zone](timezone.md)
+- [Cycle Analytics](../user/project/cycle_analytics.md)
- [Description templates](../user/project/description_templates.md)
- [Feature branch workflow](workflow.md)
- [GitLab Flow](gitlab_flow.md)
@@ -18,14 +19,18 @@
- [Slash commands](../user/project/slash_commands.md)
- [Sharing a project with a group](share_with_group.md)
- [Share projects with other groups](share_projects_with_other_groups.md)
-- [Web Editor](web_editor.md)
+- [Web Editor](../user/project/repository/web_editor.md)
- [Releases](releases.md)
- [Milestones](milestones.md)
-- [Merge Requests](merge_requests.md)
-- [Revert changes](revert_changes.md)
-- [Cherry-pick changes](cherry_pick_changes.md)
-- ["Work In Progress" Merge Requests](wip_merge_requests.md)
-- [Merge When Build Succeeds](merge_when_build_succeeds.md)
+- [Merge Requests](../user/project/merge_requests.md)
+ - [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md)
+ - [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md)
+ - [Merge when build succeeds](../user/project/merge_requests/merge_when_build_succeeds.md)
+ - [Resolve discussion comments in merge requests reviews](../user/project/merge_requests/merge_request_discussion_resolution.md)
+ - [Resolve merge conflicts in the UI](../user/project/merge_requests/resolve_conflicts.md)
+ - [Revert changes in the UI](../user/project/merge_requests/revert_changes.md)
+ - [Merge requests versions](../user/project/merge_requests/versions.md)
+ - ["Work In Progress" merge requests](../user/project/merge_requests/work_in_progress_merge_requests.md)
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
- [Importing from SVN, GitHub, BitBucket, etc](importing/README.md)
- [Todos](todos.md)
diff --git a/doc/workflow/authorization_for_merge_requests.md b/doc/workflow/authorization_for_merge_requests.md
index d1d6d94ec11..7bf80a3ad0d 100644
--- a/doc/workflow/authorization_for_merge_requests.md
+++ b/doc/workflow/authorization_for_merge_requests.md
@@ -1,40 +1 @@
-# Authorization for Merge requests
-
-There are two main ways to have a merge request flow with GitLab: working with protected branches in a single repository, or working with forks of an authoritative project.
-
-## Protected branch flow
-
-With the protected branch flow everybody works within the same GitLab project.
-
-The project maintainers get Master access and the regular developers get Developer access.
-
-The maintainers mark the authoritative branches as 'Protected'.
-
-The developers push feature branches to the project and create merge requests to have their feature branches reviewed and merged into one of the protected branches.
-
-Only users with Master access can merge changes into a protected branch.
-
-### Advantages
-
-- fewer projects means less clutter
-- developers need to consider only one remote repository
-
-### Disadvantages
-
-- manual setup of protected branch required for each new project
-
-## Forking workflow
-
-With the forking workflow the maintainers get Master access and the regular developers get Reporter access to the authoritative repository, which prohibits them from pushing any changes to it.
-
-Developers create forks of the authoritative project and push their feature branches to their own forks.
-
-To get their changes into master they need to create a merge request across forks.
-
-### Advantages
-
-- in an appropriately configured GitLab group, new projects automatically get the required access restrictions for regular developers: fewer manual steps to configure authorization for new projects
-
-### Disadvantages
-
-- the project need to keep their forks up to date, which requires more advanced Git skills (managing multiple remotes)
+This document was moved to [user/project/merge_requests/authorization_for_merge_requests](../user/project/merge_requests/authorization_for_merge_requests.md)
diff --git a/doc/workflow/cherry_pick_changes.md b/doc/workflow/cherry_pick_changes.md
index 64b94d81024..663ffd3f746 100644
--- a/doc/workflow/cherry_pick_changes.md
+++ b/doc/workflow/cherry_pick_changes.md
@@ -1,52 +1 @@
-# Cherry-pick changes
-
-> [Introduced][ce-3514] in GitLab 8.7.
-
----
-
-GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick]
-with introducing a **Cherry-pick** button in Merge Requests and commit details.
-
-## Cherry-picking a Merge Request
-
-After the Merge Request has been merged, a **Cherry-pick** button will be available
-to cherry-pick the changes introduced by that Merge Request:
-
-![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png)
-
----
-
-You can cherry-pick the changes directly into the selected branch or you can opt to
-create a new Merge Request with the cherry-pick changes:
-
-![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png)
-
-## Cherry-picking a Commit
-
-You can cherry-pick a Commit from the Commit details page:
-
-![Cherry-pick commit](img/cherry_pick_changes_commit.png)
-
----
-
-Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes
-directly into the target branch or create a new Merge Request to cherry-pick the
-changes:
-
-![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png)
-
----
-
-Please note that when cherry-picking merge commits, the mainline will always be the
-first parent. If you want to use a different mainline then you need to do that
-from the command line.
-
-Here is a quick example to cherry-pick a merge commit using the second parent as the
-mainline:
-
-```bash
-git cherry-pick -m 2 7a39eb0
-```
-
-[ce-3514]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3514 "Cherry-pick button Merge Request"
-[git-cherry-pick]: https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation"
+This document was moved to [user/project/merge_requests/cherry_pick_changes](../user/project/merge_requests/cherry_pick_changes.md).
diff --git a/doc/workflow/importing/img/import_projects_from_github_importer.png b/doc/workflow/importing/img/import_projects_from_github_importer.png
index b6ed8dd692a..eadd33c695f 100644
--- a/doc/workflow/importing/img/import_projects_from_github_importer.png
+++ b/doc/workflow/importing/img/import_projects_from_github_importer.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
index c8f35a50f48..6e91c430a33 100644
--- a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
+++ b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
new file mode 100644
index 00000000000..c11863ab10c
--- /dev/null
+++ b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
Binary files differ
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index 306caabf6e6..c36dfdb78ec 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -1,53 +1,118 @@
# Import your project from GitHub to GitLab
+Import your projects from GitHub to GitLab with minimal effort.
+
+## Overview
+
>**Note:**
-In order to enable the GitHub import setting, you may also want to
-enable the [GitHub integration][gh-import] in your GitLab instance. This
-configuration is optional, you will be able import your GitHub repositories
-with a Personal Access Token.
+If you are an administrator you can enable the [GitHub integration][gh-import]
+in your GitLab instance sitewide. This configuration is optional, users will be
+able import their GitHub repositories with a [personal access token][gh-token].
-At its current state, GitHub importer can import:
+- At its current state, GitHub importer can import:
+ - the repository description (GitLab 7.7+)
+ - the Git repository data (GitLab 7.7+)
+ - the issues (GitLab 7.7+)
+ - the pull requests (GitLab 8.4+)
+ - the wiki pages (GitLab 8.4+)
+ - the milestones (GitLab 8.7+)
+ - the labels (GitLab 8.7+)
+ - the release note descriptions (GitLab 8.12+)
+- References to pull requests and issues are preserved (GitLab 8.7+)
+- Repository public access is retained. If a repository is private in GitHub
+ it will be created as private in GitLab as well.
-- the repository description (introduced in GitLab 7.7)
-- the git repository data (introduced in GitLab 7.7)
-- the issues (introduced in GitLab 7.7)
-- the pull requests (introduced in GitLab 8.4)
-- the wiki pages (introduced in GitLab 8.4)
-- the milestones (introduced in GitLab 8.7)
-- the labels (introduced in GitLab 8.7)
+## How it works
-With GitLab 8.7+, references to pull requests and issues are preserved.
+When issues/pull requests are being imported, the GitHub importer tries to find
+the GitHub author/assignee in GitLab's database using the GitHub ID. For this
+to work, the GitHub author/assignee should have signed in beforehand in GitLab
+and [**associated their GitHub account**][social sign-in]. If the user is not
+found in GitLab's database, the project creator (most of the times the current
+user that started the import process) is set as the author, but a reference on
+the issue about the original GitHub author is kept.
-The importer page is visible when you [create a new project][new-project].
-Click on the **GitHub** link and, if you are logged in via the GitHub
-integration, you will be redirected to GitHub for permission to access your
-projects. After accepting, you'll be automatically redirected to the importer.
+The importer will create any new namespaces (groups) if they don't exist or in
+the case the namespace is taken, the repository will be imported under the user's
+namespace that started the import process.
-If you are not using the GitHub integration, you can still perform a one-off
-authorization with GitHub to access your projects.
+## Importing your GitHub repositories
-Alternatively, you can also enter a GitHub Personal Access Token. Once you enter
-your token, you'll be taken to the importer.
+The importer page is visible when you create a new project.
![New project page on GitLab](img/import_projects_from_github_new_project_page.png)
----
+Click on the **GitHub** link and the import authorization process will start.
+There are two ways to authorize access to your GitHub repositories:
+
+1. [Using the GitHub integration][gh-integration] (if it's enabled by your
+ GitLab administrator). This is the preferred way as it's possible to
+ preserve the GitHub authors/assignees. Read more in the [How it works](#how-it-works)
+ section.
+1. [Using a personal access token][gh-token] provided by GitHub.
+
+![Select authentication method](img/import_projects_from_github_select_auth_method.png)
+
+### Authorize access to your repositories using the GitHub integration
+
+If the [GitHub integration][gh-import] is enabled by your GitLab administrator,
+you can use it instead of the personal access token.
+
+1. First you may want to connect your GitHub account to GitLab in order for
+ the username mapping to be correct. Follow the [social sign-in] documentation
+ on how to do so.
+1. Once you connect GitHub, click the **List your GitHub repositories** button
+ and you will be redirected to GitHub for permission to access your projects.
+1. After accepting, you'll be automatically redirected to the importer.
+
+You can now go on and [select which repositories to import](#select-which-repositories-to-import).
+
+### Authorize access to your repositories using a personal access token
+
+>**Note:**
+For a proper author/assignee mapping for issues and pull requests, the
+[GitHub integration][gh-integration] should be used instead of the
+[personal access token][gh-token]. If the GitHub integration is enabled by your
+GitLab administrator, it should be the preferred method to import your repositories.
+Read more in the [How it works](#how-it-works) section.
+
+If you are not using the GitHub integration, you can still perform a one-off
+authorization with GitHub to grant GitLab access your repositories:
+
+1. Go to <https://github.com/settings/tokens/new>.
+1. Enter a token description.
+1. Check the `repo` scope.
+1. Click **Generate token**.
+1. Copy the token hash.
+1. Go back to GitLab and provide the token to the GitHub importer.
+1. Hit the **List your GitHub repositories** button and wait while GitLab reads
+ your repositories' information. Once done, you'll be taken to the importer
+ page to select the repositories to import.
+
+### Select which repositories to import
+
+After you've authorized access to your GitHub repositories, you will be
+redirected to the GitHub importer page.
+
+From there, you can see the import statuses of your GitHub repositories.
+
+- Those that are being imported will show a _started_ status,
+- those already successfully imported will be green with a _done_ status,
+- whereas those that are not yet imported will have an **Import** button on the
+ right side of the table.
-While at the GitHub importer page, you can see the import statuses of your
-GitHub projects. Those that are being imported will show a _started_ status,
-those already imported will be green, whereas those that are not yet imported
-have an **Import** button on the right side of the table. If you want, you can
-import all your GitHub projects in one go by hitting **Import all projects**
-in the upper left corner.
+If you want, you can import all your GitHub projects in one go by hitting
+**Import all projects** in the upper left corner.
![GitHub importer page](img/import_projects_from_github_importer.png)
---
-The importer will create any new namespaces if they don't exist or in the
-case the namespace is taken, the project will be imported on the user's
-namespace.
+You can also choose a different name for the project and a different namespace,
+if you have the privileges to do so.
[gh-import]: ../../integration/github.md "GitHub integration"
-[ee-gh]: http://docs.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE"
[new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab"
+[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
+[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
+[social sign-in]: ../../profile/account/social_sign_in.md
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 9dc1e9b47e3..b3c73e947f0 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -45,5 +45,5 @@ In `config/gitlab.yml`:
* 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
-* LFS authentications via SSH is not supported for the time being
-* Only compatible with the GitLFS client versions 1.1.0 or 1.0.2.
+* LFS authentications via SSH was added with GitLab 8.12
+* Only compatible with the GitLFS client versions 1.1.0 and up, or 1.0.2.
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 9fe065fa680..1a4f213a792 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -35,6 +35,10 @@ Documentation for GitLab instance administrators is under [LFS administration do
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)
+
+>**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+ still goes over HTTP, but now the SSH client passes the correct credentials
+ to the Git LFS client, so no action is required by the user.
## Using Git LFS
@@ -132,6 +136,10 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
### Credentials are always required when pushing an object
+>**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+ still goes over HTTP, but now the SSH client passes the correct credentials
+ to the Git LFS client, so no action is required by the user.
+
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.
diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md
index 40a5e4476be..a68bb8b27ca 100644
--- a/doc/workflow/merge_requests.md
+++ b/doc/workflow/merge_requests.md
@@ -1,90 +1 @@
-# Merge Requests
-
-Merge requests allow you to exchange changes you made to source code
-
-## Only allow merge requests to be merged if the build succeeds
-
-You can prevent merge requests from being merged if their build did not succeed
-in the project settings page.
-
-![only_allow_merge_if_build_succeeds](merge_requests/only_allow_merge_if_build_succeeds.png)
-
-Navigate to project settings page and select the `Only allow merge requests to be merged if the build succeeds` check box.
-
-Please note that you need to have builds configured to enable this feature.
-
-## Checkout merge requests locally
-
-### By adding a git alias
-
-Add the following alias to your `~/.gitconfig`:
-
-```
-[alias]
- mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
-```
-
-Now you can check out a particular merge request from any repository and any remote, e.g. to check out a merge request number 5 as shown in GitLab from the `upstream` remote, do:
-
-```
-$ git mr upstream 5
-```
-
-This will fetch the merge request into a local `mr-upstream-5` branch and check it out.
-
-### By modifying `.git/config` for a given repository
-
-Locate the section for your GitLab remote in the `.git/config` file. It looks like this:
-
-```
-[remote "origin"]
- url = https://gitlab.com/gitlab-org/gitlab-ce.git
- fetch = +refs/heads/*:refs/remotes/origin/*
-```
-
-Now add the line `fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*` to this section.
-
-It should look like this:
-
-```
-[remote "origin"]
- url = https://gitlab.com/gitlab-org/gitlab-ce.git
- fetch = +refs/heads/*:refs/remotes/origin/*
- fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
-```
-
-Now you can fetch all the merge requests:
-
-```
-$ git fetch origin
-From https://gitlab.com/gitlab-org/gitlab-ce.git
- * [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
- * [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
-...
-```
-
-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)
-
-If you click the "Hide whitespace changes" button, you can see the 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)
-
-## Merge Requests versions
-
-Every time you push to merge request branch, a new version of merge request diff
-is created. When you visit the merge request page you see latest version of changes.
-However you can select an older one from version dropdown
-
-![Merge Request Versions](merge_requests/versions.png)
+This document was moved to [user/project/merge_requests](../user/project/merge_requests.md).
diff --git a/doc/workflow/merge_requests/merge_request_diff.png b/doc/workflow/merge_requests/merge_request_diff.png
deleted file mode 100644
index 3ebbfb75ea3..00000000000
--- a/doc/workflow/merge_requests/merge_request_diff.png
+++ /dev/null
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
deleted file mode 100644
index a0db535019c..00000000000
--- a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/merge_requests/versions.png b/doc/workflow/merge_requests/versions.png
deleted file mode 100644
index c0a6dfe6806..00000000000
--- a/doc/workflow/merge_requests/versions.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/merge_when_build_succeeds.md b/doc/workflow/merge_when_build_succeeds.md
index 75e1fdff2b2..95afd12ebdb 100644
--- a/doc/workflow/merge_when_build_succeeds.md
+++ b/doc/workflow/merge_when_build_succeeds.md
@@ -1,15 +1 @@
-# Merge When Build Succeeds
-
-When reviewing a merge request that looks ready to merge but still has one or more CI builds running, you can set it to be merged automatically when all builds succeed. This way, you don't have to wait for the builds to finish and remember to merge the request manually.
-
-![Enable](merge_when_build_succeeds/enable.png)
-
-When you hit the "Merge When Build Succeeds" button, the status of the merge request will be updated to represent the impending merge. If you cannot wait for the build to succeed and want to merge immediately, this option is available in the dropdown menu on the right of the main button.
-
-Both team developers and the author of the merge request have the option to cancel the automatic merge if they find a reason why it shouldn't be merged after all.
-
-![Status](merge_when_build_succeeds/status.png)
-
-When the build succeeds, the merge request will automatically be merged. When the build fails, the author gets a chance to retry any failed builds, or to push new commits to fix the failure.
-
-When the builds are retried and succeed on the second try, the merge request will automatically be merged after all. When the merge request is updated with new commits, the automatic merge is automatically canceled to allow the new changes to be reviewed.
+This document was moved to [user/project/merge_requests/merge_when_build_succeeds](../user/project/merge_requests/merge_when_build_succeeds.md).
diff --git a/doc/workflow/revert_changes.md b/doc/workflow/revert_changes.md
index 5ead9f4177f..cf1292253fc 100644
--- a/doc/workflow/revert_changes.md
+++ b/doc/workflow/revert_changes.md
@@ -1,64 +1 @@
-# Reverting changes
-
-> [Introduced][ce-1990] in GitLab 8.5.
-
----
-
-GitLab implements Git's powerful feature to [revert any commit][git-revert]
-with introducing a **Revert** button in Merge Requests and commit details.
-
-## Reverting a Merge Request
-
-_**Note:** The **Revert** button will only be available for Merge Requests
-created since GitLab 8.5. However, you can still revert a Merge Request
-by reverting the merge commit from the list of Commits page._
-
-After the Merge Request has been merged, a **Revert** button will be available
-to revert the changes introduced by that Merge Request:
-
-![Revert Merge Request](img/revert_changes_mr.png)
-
----
-
-You can revert the changes directly into the selected branch or you can opt to
-create a new Merge Request with the revert changes:
-
-![Revert Merge Request modal](img/revert_changes_mr_modal.png)
-
----
-
-After the Merge Request has been reverted, the **Revert** button will not be
-available anymore.
-
-## Reverting a Commit
-
-You can revert a Commit from the Commit details page:
-
-![Revert commit](img/revert_changes_commit.png)
-
----
-
-Similar to reverting a Merge Request, you can opt to revert the changes
-directly into the target branch or create a new Merge Request to revert the
-changes:
-
-![Revert commit modal](img/revert_changes_commit_modal.png)
-
----
-
-After the Commit has been reverted, the **Revert** button will not be available
-anymore.
-
-Please note that when reverting merge commits, the mainline will always be the
-first parent. If you want to use a different mainline then you need to do that
-from the command line.
-
-Here is a quick example to revert a merge commit using the second parent as the
-mainline:
-
-```bash
-git revert -m 2 7a39eb0
-```
-
-[ce-1990]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1990 "Revert button Merge Request"
-[git-revert]: https://git-scm.com/docs/git-revert "Git revert documentation"
+This document was moved to [user/project/merge_requests/revert_changes](../user/project/merge_requests/revert_changes.md).
diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md
index ee8e7862572..595c7da155b 100644
--- a/doc/workflow/web_editor.md
+++ b/doc/workflow/web_editor.md
@@ -1,151 +1 @@
-# GitLab Web Editor
-
-Sometimes it's easier to make quick changes directly from the GitLab interface
-than to clone the project and use the Git command line tool. In this feature
-highlight we look at how you can create a new file, directory, branch or
-tag from the file browser. All of these actions are available from a single
-dropdown menu.
-
-## Create a file
-
-From a project's files page, click the '+' button to the right of the branch selector.
-Choose **New file** from the dropdown.
-
-![New file dropdown menu](img/web_editor_new_file_dropdown.png)
-
----
-
-Enter a file name in the **File name** box. Then, add file content in the editor
-area. Add a descriptive commit message and choose a branch. The branch field
-will default to the branch you were viewing in the file browser. If you enter
-a new branch name, a checkbox will appear allowing you to start a new merge
-request after you commit the changes.
-
-When you are satisfied with your new file, click **Commit Changes** at the bottom.
-
-![Create file editor](img/web_editor_new_file_editor.png)
-
-## Upload a file
-
-The ability to create a file is great when the content is text. However, this
-doesn't work well for binary data such as images, PDFs or other file types. In
-this case you need to upload a file.
-
-From a project's files page, click the '+' button to the right of the branch
-selector. Choose **Upload file** from the dropdown.
-
-![Upload file dropdown menu](img/web_editor_upload_file_dropdown.png)
-
----
-
-Once the upload dialog pops up there are two ways to upload your file. Either
-drag and drop a file on the pop up or use the **click to upload** link. A file
-preview will appear once you have selected a file to upload.
-
-Enter a commit message, choose a branch, and click **Upload file** when you are
-ready.
-
-![Upload file dialog](img/web_editor_upload_file_dialog.png)
-
-## Create a directory
-
-To keep files in the repository organized it is often helpful to create a new
-directory.
-
-From a project's files page, click the '+' button to the right of the branch selector.
-Choose **New directory** from the dropdown.
-
-![New directory dropdown](img/web_editor_new_directory_dropdown.png)
-
----
-
-In the new directory dialog enter a directory name, a commit message and choose
-the target branch. Click **Create directory** to finish.
-
-![New directory dialog](img/web_editor_new_directory_dialog.png)
-
-## Create a new branch
-
-There are multiple ways to create a branch from GitLab's web interface.
-
-### Create a new branch from an issue
-
-> [Introduced][ce-2808] in GitLab 8.6.
-
-In case your development workflow dictates to have an issue for every merge
-request, you can quickly create a branch right on the issue page which will be
-tied with the issue itself. You can see a **New Branch** button after the issue
-description, unless there is already a branch with the same name or a referenced
-merge request.
-
-![New Branch Button](img/new_branch_from_issue.png)
-
-Once you click it, a new branch will be created that diverges from the default
-branch of your project, by default `master`. The branch name will be based on
-the title of the issue and as suffix it will have its ID. Thus, the example
-screenshot above will yield a branch named
-`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`.
-
-After the branch is created, you can edit files in the repository to fix
-the issue. When a merge request is created based on the newly created branch,
-the description field will automatically display the [issue closing pattern]
-`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the
-merge request is merged.
-
-### Create a new branch from a project's dashboard
-
-If you want to make changes to several files before creating a new merge
-request, you can create a new branch up front. From a project's files page,
-choose **New branch** from the dropdown.
-
-![New branch dropdown](img/web_editor_new_branch_dropdown.png)
-
----
-
-Enter a new **Branch name**. Optionally, change the **Create from** field
-to choose which branch, tag or commit SHA this new branch will originate from.
-This field will autocomplete if you start typing an existing branch or tag.
-Click **Create branch** and you will be returned to the file browser on this new
-branch.
-
-![New branch page](img/web_editor_new_branch_page.png)
-
----
-
-You can now make changes to any files, as needed. When you're ready to merge
-the changes back to master you can use the widget at the top of the screen.
-This widget only appears for a period of time after you create the branch or
-modify files.
-
-![New push widget](img/web_editor_new_push_widget.png)
-
-## Create a new tag
-
-Tags are useful for marking major milestones such as production releases,
-release candidates, and more. You can create a tag from a branch or a commit
-SHA. From a project's files page, choose **New tag** from the dropdown.
-
-![New tag dropdown](img/web_editor_new_tag_dropdown.png)
-
----
-
-Give the tag a name such as `v1.0.0`. Choose the branch or SHA from which you
-would like to create this new tag. You can optionally add a message and
-release notes. The release notes section supports markdown format and you can
-also upload an attachment. Click **Create tag** and you will be taken to the tag
-list page.
-
-![New tag page](img/web_editor_new_tag_page.png)
-
-## Tips
-
-When creating or uploading a new file, or creating a new directory, you can
-trigger a new merge request rather than committing directly to master. Enter
-a new branch name in the **Target branch** field. You will notice a checkbox
-appear that is labeled **Start a new merge request with these changes**. After
-you commit the changes you will be taken to a new merge request form.
-
-![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png)
-
-[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
-[issue closing pattern]: ../customization/issue_closing.md
+This document was moved to [user/project/repository/web_editor](../user/project/repository/web_editor.md).
diff --git a/doc/workflow/wip_merge_requests.md b/doc/workflow/wip_merge_requests.md
index 46035a5e6b6..abb8002f442 100644
--- a/doc/workflow/wip_merge_requests.md
+++ b/doc/workflow/wip_merge_requests.md
@@ -1,13 +1 @@
-# "Work In Progress" Merge Requests
-
-To prevent merge requests from accidentally being accepted before they're completely ready, GitLab blocks the "Accept" button for merge requests that have been marked a **Work In Progress**.
-
-![Blocked Accept Button](wip_merge_requests/blocked_accept_button.png)
-
-To mark a merge request a Work In Progress, simply start its title with `[WIP]` or `WIP:`.
-
-![Mark as WIP](wip_merge_requests/mark_as_wip.png)
-
-To allow a Work In Progress merge request to be accepted again when it's ready, simply remove the `WIP` prefix.
-
-![Unark as WIP](wip_merge_requests/unmark_as_wip.png)
+This document was moved to [user/project/merge_requests/work_in_progress_merge_requests](../user/project/merge_requests/work_in_progress_merge_requests.md).
diff --git a/features/project/snippets.feature b/features/project/snippets.feature
index 270557cbde7..3c51ea56585 100644
--- a/features/project/snippets.feature
+++ b/features/project/snippets.feature
@@ -12,7 +12,7 @@ Feature: Project Snippets
And I should not see "Snippet two" in snippets
Scenario: I create new project snippet
- Given I click link "New Snippet"
+ Given I click link "New snippet"
And I submit new snippet "Snippet three"
Then I should see snippet "Snippet three"
diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb
index 03f87df7a60..11dc7f580f0 100644
--- a/features/steps/admin/settings.rb
+++ b/features/steps/admin/settings.rb
@@ -33,6 +33,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
page.check('Issue')
page.check('Merge request')
page.check('Build')
+ page.check('Pipeline')
click_on 'Save'
end
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index 4ee6784a086..05ab2a7dc73 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -13,6 +13,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
fill_in 'user_website_url', with: 'testurl'
fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab'
+ fill_in 'user_organization', with: 'GitLab'
click_button 'Update profile settings'
@user.reload
end
@@ -23,6 +24,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
expect(@user.twitter).to eq 'testtwitter'
expect(@user.website_url).to eq 'testurl'
expect(@user.bio).to eq 'I <3 GitLab'
+ expect(@user.organization).to eq 'GitLab'
expect(find('#user_location').value).to eq 'Ukraine'
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 99bd2ac3bc0..b50f5238e80 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -357,6 +357,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
def filter_issue(text)
- fill_in 'issue_search', with: text
+ fill_in 'issuable_search', with: text
end
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 56b28949585..4a67cf06fba 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -31,7 +31,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I click link "Closed"' do
- click_link "Closed"
+ page.within('.issues-state-filters') do
+ click_link "Closed"
+ end
end
step 'I should see merge request "Wiki Feature"' do
@@ -491,7 +493,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I fill in merge request search with "Fe"' do
- fill_in 'issue_search', with: "Fe"
+ fill_in 'issuable_search', with: "Fe"
end
step 'I click the "Target branch" dropdown' do
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index beb8ecfc799..5e7d539add6 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -21,8 +21,8 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
author: project.users.first)
end
- step 'I click link "New Snippet"' do
- click_link "New Snippet"
+ step 'I click link "New snippet"' do
+ click_link "New snippet"
end
step 'I click link "Snippet one"' do
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index d02b469dac8..7b9de7c9598 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -16,11 +16,11 @@ module API
# GET /projects/:id/access_requests
get ":id/access_requests" do
source = find_source(source_type, params[:id])
- authorize_admin_source!(source_type, source)
- access_requesters = paginate(source.requesters.includes(:user))
+ access_requesters = AccessRequestsFinder.new(source).execute!(current_user)
+ access_requesters = paginate(access_requesters.includes(:user))
- present access_requesters.map(&:user), with: Entities::AccessRequester, access_requesters: access_requesters
+ present access_requesters.map(&:user), with: Entities::AccessRequester, source: source
end
# Request access to the group/project
@@ -55,13 +55,8 @@ module API
put ':id/access_requests/:user_id/approve' do
required_attributes! [:user_id]
source = find_source(source_type, params[:id])
- authorize_admin_source!(source_type, source)
- member = source.requesters.find_by!(user_id: params[:user_id])
- if params[:access_level]
- member.update(access_level: params[:access_level])
- end
- member.accept_request
+ member = ::Members::ApproveAccessRequestService.new(source, current_user, params).execute
status :created
present member.user, with: Entities::Member, member: member
diff --git a/lib/api/api.rb b/lib/api/api.rb
index e14464c1b0d..cb47ec8f33f 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -28,6 +28,7 @@ module API
helpers ::SentryHelper
helpers ::API::Helpers
+ # Keep in alphabetical order
mount ::API::AccessRequests
mount ::API::AwardEmoji
mount ::API::Branches
@@ -45,11 +46,14 @@ module API
mount ::API::Keys
mount ::API::Labels
mount ::API::LicenseTemplates
+ mount ::API::Lint
mount ::API::Members
mount ::API::MergeRequests
+ mount ::API::MergeRequestDiffs
mount ::API::Milestones
mount ::API::Namespaces
mount ::API::Notes
+ mount ::API::NotificationSettings
mount ::API::Pipelines
mount ::API::ProjectHooks
mount ::API::ProjectSnippets
@@ -68,6 +72,5 @@ module API
mount ::API::Triggers
mount ::API::Users
mount ::API::Variables
- mount ::API::MergeRequestDiffs
end
end
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 7e67edb203a..8cc7a26f1fa 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -33,46 +33,29 @@ module API
#
# If the token is revoked, then it raises RevokedError.
#
- # If the token is not found (nil), then it raises TokenNotFoundError.
+ # If the token is not found (nil), then it returns nil
#
# Arguments:
#
# scopes: (optional) scopes required for this guard.
# Defaults to empty array.
#
- def doorkeeper_guard!(scopes: [])
- if (access_token = find_access_token).nil?
- raise TokenNotFoundError
-
- else
- case validate_access_token(access_token, scopes)
- when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
- raise InsufficientScopeError.new(scopes)
- when Oauth2::AccessTokenValidationService::EXPIRED
- raise ExpiredError
- when Oauth2::AccessTokenValidationService::REVOKED
- raise RevokedError
- when Oauth2::AccessTokenValidationService::VALID
- @current_user = User.find(access_token.resource_owner_id)
- end
- end
- end
-
def doorkeeper_guard(scopes: [])
- if access_token = find_access_token
- case validate_access_token(access_token, scopes)
- when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
- raise InsufficientScopeError.new(scopes)
+ access_token = find_access_token
+ return nil unless access_token
+
+ case validate_access_token(access_token, scopes)
+ when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
+ raise InsufficientScopeError.new(scopes)
- when Oauth2::AccessTokenValidationService::EXPIRED
- raise ExpiredError
+ when Oauth2::AccessTokenValidationService::EXPIRED
+ raise ExpiredError
- when Oauth2::AccessTokenValidationService::REVOKED
- raise RevokedError
+ when Oauth2::AccessTokenValidationService::REVOKED
+ raise RevokedError
- when Oauth2::AccessTokenValidationService::VALID
- @current_user = User.find(access_token.resource_owner_id)
- end
+ when Oauth2::AccessTokenValidationService::VALID
+ @current_user = User.find(access_token.resource_owner_id)
end
end
@@ -96,19 +79,6 @@ module API
end
module ClassMethods
- # Installs the doorkeeper guard on the whole Grape API endpoint.
- #
- # Arguments:
- #
- # scopes: (optional) scopes required for this guard.
- # Defaults to empty array.
- #
- def guard_all!(scopes: [])
- before do
- guard! scopes: scopes
- end
- end
-
private
def install_error_responders(base)
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 7c22b17e4e5..2461a783ea8 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -1,12 +1,12 @@
module API
class AwardEmoji < Grape::API
before { authenticate! }
- AWARDABLES = [Issue, MergeRequest]
+ AWARDABLES = %w[issue merge_request snippet]
resource :projects do
AWARDABLES.each do |awardable_type|
- awardable_string = awardable_type.to_s.underscore.pluralize
- awardable_id_string = "#{awardable_type.to_s.underscore}_id"
+ awardable_string = awardable_type.pluralize
+ awardable_id_string = "#{awardable_type}_id"
[ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
@@ -87,9 +87,7 @@ module API
helpers do
def can_read_awardable?
- ability = "read_#{awardable.class.to_s.underscore}".to_sym
-
- can?(current_user, ability, awardable)
+ can?(current_user, read_ability(awardable), awardable)
end
def can_award_awardable?
@@ -100,18 +98,25 @@ module API
@awardable ||=
begin
if params.include?(:note_id)
- noteable.notes.find(params[:note_id])
+ note_id = params.delete(:note_id)
+
+ awardable.notes.find(note_id)
+ elsif params.include?(:issue_id)
+ user_project.issues.find(params[:issue_id])
+ elsif params.include?(:merge_request_id)
+ user_project.merge_requests.find(params[:merge_request_id])
else
- noteable
+ user_project.snippets.find(params[:snippet_id])
end
end
end
- def noteable
- if params.include?(:issue_id)
- user_project.issues.find(params[:issue_id])
+ def read_ability(awardable)
+ case awardable
+ when Note
+ read_ability(awardable.noteable)
else
- user_project.merge_requests.find(params[:merge_request_id])
+ :"read_#{awardable.class.to_s.underscore}"
end
end
end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 5e3c9563703..dfbdd597d29 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -37,7 +37,7 @@ module API
# 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
+ # state (required) - The state of the status. Can be: pending, running, success, failed or canceled
# 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"
@@ -46,7 +46,7 @@ module API
post ':id/statuses/:sha' do
authorize! :create_commit_status, user_project
required_attributes! [:state]
- attrs = attributes_for_keys [:ref, :target_url, :description, :context, :name]
+ attrs = attributes_for_keys [:target_url, :description]
commit = @project.commit(params[:sha])
not_found! 'Commit' unless commit
@@ -58,36 +58,38 @@ module API
# the first found branch on that commit
ref = params[:ref]
- unless ref
- branches = @project.repository.branch_names_contains(commit.sha)
- not_found! 'References for commit' if branches.none?
- ref = branches.first
- end
+ ref ||= @project.repository.branch_names_contains(commit.sha).first
+ not_found! 'References for commit' unless ref
+
+ name = params[:name] || params[:context] || 'default'
pipeline = @project.ensure_pipeline(ref, commit.sha, current_user)
- name = params[:name] || params[:context]
- status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref])
- status ||= GenericCommitStatus.new(project: @project, pipeline: pipeline, user: current_user)
- status.update(attrs)
+ status = GenericCommitStatus.running_or_pending.find_or_initialize_by(
+ project: @project, pipeline: pipeline,
+ user: current_user, name: name, ref: ref)
+ status.attributes = 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
+ begin
+ case params[:state].to_s
+ when 'pending'
+ status.enqueue!
+ when 'running'
+ status.enqueue
+ status.run!
+ when 'success'
+ status.success!
+ when 'failed'
+ status.drop!
+ when 'canceled'
+ status.cancel!
+ else
+ render_api_error!('invalid state', 400)
+ end
- if status.save
present status, with: Entities::CommitStatus
- else
- render_validation_error!(status)
+ rescue StateMachines::InvalidTransition => e
+ render_api_error!(e.message, 400)
end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 3faba79415b..04437322ec1 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -15,7 +15,7 @@ module API
class User < UserBasic
expose :created_at
expose :is_admin?, as: :is_admin
- expose :bio, :location, :skype, :linkedin, :twitter, :website_url
+ expose :bio, :location, :skype, :linkedin, :twitter, :website_url, :organization
end
class Identity < Grape::Entity
@@ -86,7 +86,8 @@ module API
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:user]) }
expose :created_at, :last_activity_at
- expose :shared_runners_enabled, :lfs_enabled
+ expose :shared_runners_enabled
+ expose :lfs_enabled?, as: :lfs_enabled
expose :creator_id
expose :namespace
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
@@ -99,30 +100,33 @@ module API
SharedGroup.represent(project.project_group_links.all, options)
end
expose :only_allow_merge_if_build_succeeds
+ expose :request_access_enabled
end
class Member < UserBasic
expose :access_level do |user, options|
- member = options[:member] || options[:members].find { |m| m.user_id == user.id }
+ member = options[:member] || options[:source].members.find_by(user_id: user.id)
member.access_level
end
expose :expires_at do |user, options|
- member = options[:member] || options[:members].find { |m| m.user_id == user.id }
+ member = options[:member] || options[:source].members.find_by(user_id: user.id)
member.expires_at
end
end
class AccessRequester < UserBasic
expose :requested_at do |user, options|
- access_requester = options[:access_requester] || options[:access_requesters].find { |m| m.user_id == user.id }
+ access_requester = options[:access_requester] || options[:source].requesters.find_by(user_id: user.id)
access_requester.requested_at
end
end
class Group < Grape::Entity
expose :id, :name, :path, :description, :visibility_level
+ expose :lfs_enabled?, as: :lfs_enabled
expose :avatar_url
expose :web_url
+ expose :request_access_enabled
end
class GroupDetail < Group
@@ -339,7 +343,7 @@ module API
end
class ProjectGroupLink < Grape::Entity
- expose :id, :project_id, :group_id, :group_access
+ expose :id, :project_id, :group_id, :group_access, :expires_at
end
class Todo < Grape::Entity
@@ -375,7 +379,7 @@ module API
expose :access_level
expose :notification_level do |member, options|
if member.notification_setting
- NotificationSetting.levels[member.notification_setting.level]
+ ::NotificationSetting.levels[member.notification_setting.level]
end
end
end
@@ -386,6 +390,21 @@ module API
class GroupAccess < MemberAccess
end
+ class NotificationSetting < Grape::Entity
+ expose :level
+ expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do
+ ::NotificationSetting::EMAIL_EVENTS.each do |event|
+ expose event
+ end
+ end
+ end
+
+ class GlobalNotificationSetting < NotificationSetting
+ expose :notification_email do |notification_setting, options|
+ notification_setting.user.notification_email
+ end
+ end
+
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :merge_requests_events
@@ -475,6 +494,8 @@ module API
expose :after_sign_out_path
expose :container_registry_token_expire_delay
expose :repository_storage
+ expose :koding_enabled
+ expose :koding_url
end
class Release < Grape::Entity
@@ -526,6 +547,10 @@ module API
expose :filename, :size
end
+ class PipelineBasic < Grape::Entity
+ expose :id, :sha, :ref, :status
+ end
+
class Build < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
@@ -533,6 +558,7 @@ module API
expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
expose :commit, with: RepoCommit
expose :runner, with: Runner
+ expose :pipeline, with: PipelineBasic
end
class Trigger < Grape::Entity
@@ -543,8 +569,8 @@ module API
expose :key, :value
end
- class Pipeline < Grape::Entity
- expose :id, :status, :ref, :sha, :before_sha, :tag, :yaml_errors
+ class Pipeline < PipelineBasic
+ expose :before_sha, :tag, :yaml_errors
expose :user, with: Entities::UserBasic
expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
diff --git a/lib/api/files.rb b/lib/api/files.rb
index c1d86f313b0..96510e651a3 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -11,14 +11,16 @@ module API
target_branch: attrs[:branch_name],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
- file_content_encoding: attrs[:encoding]
+ file_content_encoding: attrs[:encoding],
+ author_email: attrs[:author_email],
+ author_name: attrs[:author_name]
}
end
def commit_response(attrs)
{
file_path: attrs[:file_path],
- branch_name: attrs[:branch_name],
+ branch_name: attrs[:branch_name]
}
end
end
@@ -96,7 +98,7 @@ module API
authorize! :push_code, user_project
required_attributes! [:file_path, :branch_name, :content, :commit_message]
- attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding]
+ attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding, :author_email, :author_name]
result = ::Files::CreateService.new(user_project, current_user, commit_params(attrs)).execute
if result[:status] == :success
@@ -122,7 +124,7 @@ module API
authorize! :push_code, user_project
required_attributes! [:file_path, :branch_name, :content, :commit_message]
- attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding]
+ attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding, :author_email, :author_name]
result = ::Files::UpdateService.new(user_project, current_user, commit_params(attrs)).execute
if result[:status] == :success
@@ -149,7 +151,7 @@ module API
authorize! :push_code, user_project
required_attributes! [:file_path, :branch_name, :commit_message]
- attrs = attributes_for_keys [:file_path, :branch_name, :commit_message]
+ attrs = attributes_for_keys [:file_path, :branch_name, :commit_message, :author_email, :author_name]
result = ::Files::DeleteService.new(user_project, current_user, commit_params(attrs)).execute
if result[:status] == :success
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index d2df77238d5..953fa474e88 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -23,17 +23,19 @@ module API
# Create group. Available only for users who can create groups.
#
# Parameters:
- # name (required) - The name of the group
- # path (required) - The path of the group
- # description (optional) - The description of the group
- # visibility_level (optional) - The visibility level of the group
+ # name (required) - The name of the group
+ # path (required) - The path of the group
+ # description (optional) - The description of the group
+ # visibility_level (optional) - The visibility level of the group
+ # lfs_enabled (optional) - Enable/disable LFS for the projects in this group
+ # request_access_enabled (optional) - Allow users to request member access
# Example Request:
# POST /groups
post do
authorize! :create_group
required_attributes! [:name, :path]
- attrs = attributes_for_keys [:name, :path, :description, :visibility_level]
+ attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
@group = Group.new(attrs)
if @group.save
@@ -47,17 +49,19 @@ module API
# Update group. Available only for users who can administrate groups.
#
# Parameters:
- # id (required) - The ID of a group
- # path (optional) - The path of the group
- # description (optional) - The description of the group
- # visibility_level (optional) - The visibility level of the group
+ # id (required) - The ID of a group
+ # path (optional) - The path of the group
+ # description (optional) - The description of the group
+ # visibility_level (optional) - The visibility level of the group
+ # lfs_enabled (optional) - Enable/disable LFS for the projects in this group
+ # request_access_enabled (optional) - Allow users to request member access
# Example Request:
# PUT /groups/:id
put ':id' do
group = find_group(params[:id])
authorize! :admin_group, group
- attrs = attributes_for_keys [:name, :path, :description, :visibility_level]
+ attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
if ::Groups::UpdateService.new(group, current_user, attrs).execute
present group, with: Entities::GroupDetail
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 6a20ba95a79..714d4ea3dc6 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -12,13 +12,30 @@ module API
nil
end
+ def private_token
+ params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
+ end
+
+ def warden
+ env['warden']
+ end
+
+ # Check the Rails session for valid authentication details
+ def find_user_from_warden
+ warden ? warden.authenticate : nil
+ end
+
def find_user_by_private_token
- token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
- User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
+ token = private_token
+ return nil unless token.present?
+
+ User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
end
def current_user
- @current_user ||= (find_user_by_private_token || doorkeeper_guard)
+ @current_user ||= find_user_by_private_token
+ @current_user ||= doorkeeper_guard
+ @current_user ||= find_user_from_warden
unless @current_user && Gitlab::UserAccess.new(@current_user).allowed?
return nil
@@ -269,6 +286,10 @@ module API
render_api_error!('304 Not Modified', 304)
end
+ def no_content!
+ render_api_error!('204 No Content', 204)
+ end
+
def render_validation_error!(model)
if model.errors.any?
render_api_error!(model.errors.messages || '400 Bad Request', 400)
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 5b54c11ef62..9a5d1ece070 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -35,6 +35,14 @@ module API
Project.find_with_namespace(project_path)
end
end
+
+ def ssh_authentication_abilities
+ [
+ :read_project,
+ :download_code,
+ :push_code
+ ]
+ end
end
post "/allowed" do
@@ -51,9 +59,9 @@ module API
access =
if wiki?
- Gitlab::GitAccessWiki.new(actor, project, protocol)
+ Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
else
- Gitlab::GitAccess.new(actor, project, protocol)
+ Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
end
access_status = access.check(params[:action], params[:changes])
@@ -74,6 +82,19 @@ module API
response
end
+ post "/lfs_authenticate" do
+ status 200
+
+ key = Key.find(params[:key_id])
+ token_handler = Gitlab::LfsToken.new(key)
+
+ {
+ username: token_handler.actor_name,
+ lfs_token: token_handler.token,
+ repository_http_path: project.http_url_to_repo
+ }
+ end
+
get "/merge_request_urls" do
::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
end
@@ -105,15 +126,19 @@ module API
post '/two_factor_recovery_codes' do
status 200
- key = Key.find(params[:key_id])
- user = key.user
+ key = Key.find_by(id: params[:key_id])
- # Make sure this isn't a deploy key
- unless key.type.nil?
+ unless key
+ return { 'success' => false, 'message' => 'Could not find the given key' }
+ end
+
+ if key.is_a?(DeployKey)
return { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
end
- unless user.present?
+ user = key.user
+
+ unless user
return { success: false, message: 'Could not find a user for the given key' }
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 556684187d8..c9689e6f8ef 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -41,7 +41,8 @@ module API
issues = current_user.issues.inc_notes_with_associations
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
- issues.reorder(issuable_order_by => issuable_sort)
+ issues = issues.reorder(issuable_order_by => issuable_sort)
+
present paginate(issues), with: Entities::Issue, current_user: current_user
end
end
@@ -73,7 +74,11 @@ module API
params[:group_id] = group.id
params[:milestone_title] = params.delete(:milestone)
params[:label_name] = params.delete(:labels)
- params[:sort] = "#{params.delete(:order_by)}_#{params.delete(:sort)}" if params[:order_by] && params[:sort]
+
+ if params[:order_by] || params[:sort]
+ # The Sortable concern takes 'created_desc', not 'created_at_desc' (for example)
+ params[:sort] = "#{issuable_order_by.sub('_at', '')}_#{issuable_sort}"
+ end
issues = IssuesFinder.new(current_user, params).execute
@@ -113,7 +118,8 @@ module API
issues = filter_issues_milestone(issues, params[:milestone])
end
- issues.reorder(issuable_order_by => issuable_sort)
+ issues = issues.reorder(issuable_order_by => issuable_sort)
+
present paginate(issues), with: Entities::Issue, current_user: current_user
end
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
new file mode 100644
index 00000000000..ae43a4a3237
--- /dev/null
+++ b/lib/api/lint.rb
@@ -0,0 +1,21 @@
+module API
+ class Lint < Grape::API
+ namespace :ci do
+ desc 'Validation of .gitlab-ci.yml content'
+ params do
+ requires :content, type: String, desc: 'Content of .gitlab-ci.yml'
+ end
+ post '/lint' do
+ error = Ci::GitlabCiYamlProcessor.validation_message(params[:content])
+
+ status 200
+
+ if error.blank?
+ { status: 'valid', errors: [] }
+ else
+ { status: 'invalid', errors: [error] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 94c16710d9a..a18ce769e29 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -18,11 +18,11 @@ module API
get ":id/members" do
source = find_source(source_type, params[:id])
- members = source.members.includes(:user)
- members = members.joins(:user).merge(User.search(params[:query])) if params[:query]
- members = paginate(members)
+ users = source.users
+ users = users.merge(User.search(params[:query])) if params[:query]
+ users = paginate(users)
- present members.map(&:user), with: Entities::Member, members: members
+ present users, with: Entities::Member, source: source
end
# Get a group/project member
@@ -59,13 +59,6 @@ module API
authorize_admin_source!(source_type, source)
required_attributes! [:user_id, :access_level]
- access_requester = source.requesters.find_by(user_id: params[:user_id])
- if access_requester
- # We pass current_user = access_requester so that the requester doesn't
- # receive a "access denied" email
- ::Members::DestroyService.new(access_requester, access_requester.user).execute
- end
-
member = source.members.find_by(user_id: params[:user_id])
# This is to ensure back-compatibility but 409 behavior should be used
@@ -73,18 +66,12 @@ module API
conflict!('Member already exists') if source_type == 'group' && member
unless member
- source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
- member = source.members.find_by(user_id: params[:user_id])
+ member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
end
- if member
+ if member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member
else
- # Since `source.add_user` doesn't return a member object, we have to
- # build a new one and populate its errors in order to render them.
- member = source.members.build(attributes_for_keys([:user_id, :access_level, :expires_at]))
- member.valid? # populate the errors
-
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index 7a0cb7c99f3..9b73f6826cf 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -108,8 +108,7 @@ module API
finder_params = {
project_id: user_project.id,
- milestone_title: @milestone.title,
- state: 'all'
+ milestone_title: @milestone.title
}
issues = IssuesFinder.new(current_user, finder_params).execute
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 8bfa998dc53..c5c214d4d13 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -83,12 +83,12 @@ module API
opts[:created_at] = params[:created_at]
end
- @note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
- if @note.valid?
- present @note, with: Entities::Note
+ if note.valid?
+ present note, with: Entities::const_get(note.class.name)
else
- not_found!("Note #{@note.errors.messages}")
+ not_found!("Note #{note.errors.messages}")
end
end
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
new file mode 100644
index 00000000000..a70a7e71073
--- /dev/null
+++ b/lib/api/notification_settings.rb
@@ -0,0 +1,97 @@
+module API
+ # notification_settings API
+ class NotificationSettings < Grape::API
+ before { authenticate! }
+
+ helpers ::API::Helpers::MembersHelpers
+
+ resource :notification_settings do
+ desc 'Get global notification level settings and email, defaults to Participate' do
+ detail 'This feature was introduced in GitLab 8.12'
+ success Entities::GlobalNotificationSetting
+ end
+ get do
+ notification_setting = current_user.global_notification_setting
+
+ present notification_setting, with: Entities::GlobalNotificationSetting
+ end
+
+ desc 'Update global notification level settings and email, defaults to Participate' do
+ detail 'This feature was introduced in GitLab 8.12'
+ success Entities::GlobalNotificationSetting
+ end
+ params do
+ optional :level, type: String, desc: 'The global notification level'
+ optional :notification_email, type: String, desc: 'The email address to send notifications'
+ NotificationSetting::EMAIL_EVENTS.each do |event|
+ optional event, type: Boolean, desc: 'Enable/disable this notification'
+ end
+ end
+ put do
+ notification_setting = current_user.global_notification_setting
+
+ begin
+ notification_setting.transaction do
+ new_notification_email = params.delete(:notification_email)
+ declared_params = declared(params, include_missing: false).to_h
+
+ current_user.update(notification_email: new_notification_email) if new_notification_email
+ notification_setting.update(declared_params)
+ end
+ rescue ArgumentError => e # catch level enum error
+ render_api_error! e.to_s, 400
+ end
+
+ render_validation_error! current_user
+ render_validation_error! notification_setting
+ present notification_setting, with: Entities::GlobalNotificationSetting
+ end
+ end
+
+ %w[group project].each do |source_type|
+ resource source_type.pluralize do
+ desc "Get #{source_type} level notification level settings, defaults to Global" do
+ detail 'This feature was introduced in GitLab 8.12'
+ success Entities::NotificationSetting
+ end
+ params do
+ requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME'
+ end
+ get ":id/notification_settings" do
+ source = find_source(source_type, params[:id])
+
+ notification_setting = current_user.notification_settings_for(source)
+
+ present notification_setting, with: Entities::NotificationSetting
+ end
+
+ desc "Update #{source_type} level notification level settings, defaults to Global" do
+ detail 'This feature was introduced in GitLab 8.12'
+ success Entities::NotificationSetting
+ end
+ params do
+ requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME'
+ optional :level, type: String, desc: "The #{source_type} notification level"
+ NotificationSetting::EMAIL_EVENTS.each do |event|
+ optional event, type: Boolean, desc: 'Enable/disable this notification'
+ end
+ end
+ put ":id/notification_settings" do
+ source = find_source(source_type, params.delete(:id))
+ notification_setting = current_user.notification_settings_for(source)
+
+ begin
+ declared_params = declared(params, include_missing: false).to_h
+
+ notification_setting.update(declared_params)
+ rescue ArgumentError => e # catch level enum error
+ render_api_error! e.to_s, 400
+ end
+
+ render_validation_error! notification_setting
+ present notification_setting, with: Entities::NotificationSetting
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 2aae75c471d..2a0c8e1f2c0 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -13,11 +13,14 @@ module API
params do
optional :page, type: Integer, desc: 'Page number of the current request'
optional :per_page, type: Integer, desc: 'Number of items per page'
+ optional :scope, type: String, values: ['running', 'branches', 'tags'],
+ desc: 'Either running, branches, or tags'
end
get ':id/pipelines' do
authorize! :read_pipeline, user_project
- present paginate(user_project.pipelines), with: Entities::Pipeline
+ pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
+ present paginate(pipelines), with: Entities::Pipeline
end
desc 'Gets a specific pipeline for the project' do
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index a1fd598414a..680055c95eb 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -91,8 +91,8 @@ module API
# Create new project
#
# Parameters:
- # name (required) - name for new project
- # description (optional) - short project description
+ # name (required) - name for new project
+ # description (optional) - short project description
# issues_enabled (optional)
# merge_requests_enabled (optional)
# builds_enabled (optional)
@@ -100,33 +100,35 @@ module API
# snippets_enabled (optional)
# container_registry_enabled (optional)
# shared_runners_enabled (optional)
- # namespace_id (optional) - defaults to user namespace
- # public (optional) - if true same as setting visibility_level = 20
- # visibility_level (optional) - 0 by default
+ # namespace_id (optional) - defaults to user namespace
+ # public (optional) - if true same as setting visibility_level = 20
+ # visibility_level (optional) - 0 by default
# import_url (optional)
# public_builds (optional)
# lfs_enabled (optional)
+ # request_access_enabled (optional) - Allow users to request member access
# Example Request
# POST /projects
post do
required_attributes! [:name]
- attrs = attributes_for_keys [:name,
- :path,
+ attrs = attributes_for_keys [:builds_enabled,
+ :container_registry_enabled,
:description,
+ :import_url,
:issues_enabled,
+ :lfs_enabled,
:merge_requests_enabled,
- :builds_enabled,
- :wiki_enabled,
- :snippets_enabled,
- :container_registry_enabled,
- :shared_runners_enabled,
+ :name,
:namespace_id,
+ :only_allow_merge_if_build_succeeds,
+ :path,
:public,
- :visibility_level,
- :import_url,
:public_builds,
- :only_allow_merge_if_build_succeeds,
- :lfs_enabled]
+ :request_access_enabled,
+ :shared_runners_enabled,
+ :snippets_enabled,
+ :visibility_level,
+ :wiki_enabled]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(current_user, attrs).execute
if @project.saved?
@@ -143,10 +145,10 @@ module API
# Create new project for a specified user. Only available to admin users.
#
# Parameters:
- # user_id (required) - The ID of a user
- # name (required) - name for new project
- # description (optional) - short project description
- # default_branch (optional) - 'master' by default
+ # user_id (required) - The ID of a user
+ # name (required) - name for new project
+ # description (optional) - short project description
+ # default_branch (optional) - 'master' by default
# issues_enabled (optional)
# merge_requests_enabled (optional)
# builds_enabled (optional)
@@ -154,31 +156,33 @@ module API
# snippets_enabled (optional)
# container_registry_enabled (optional)
# shared_runners_enabled (optional)
- # public (optional) - if true same as setting visibility_level = 20
+ # public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional)
# import_url (optional)
# public_builds (optional)
# lfs_enabled (optional)
+ # request_access_enabled (optional) - Allow users to request member access
# Example Request
# POST /projects/user/:user_id
post "user/:user_id" do
authenticated_as_admin!
user = User.find(params[:user_id])
- attrs = attributes_for_keys [:name,
- :description,
+ attrs = attributes_for_keys [:builds_enabled,
:default_branch,
+ :description,
+ :import_url,
:issues_enabled,
+ :lfs_enabled,
:merge_requests_enabled,
- :builds_enabled,
- :wiki_enabled,
- :snippets_enabled,
- :shared_runners_enabled,
+ :name,
+ :only_allow_merge_if_build_succeeds,
:public,
- :visibility_level,
- :import_url,
:public_builds,
- :only_allow_merge_if_build_succeeds,
- :lfs_enabled]
+ :request_access_enabled,
+ :shared_runners_enabled,
+ :snippets_enabled,
+ :visibility_level,
+ :wiki_enabled]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(user, attrs).execute
if @project.saved?
@@ -189,16 +193,32 @@ module API
end
end
- # Fork new project for the current user.
+ # Fork new project for the current user or provided namespace.
#
# Parameters:
# id (required) - The ID of a project
+ # namespace (optional) - The ID or name of the namespace that the project will be forked into.
# Example Request
# POST /projects/fork/:id
post 'fork/:id' do
+ attrs = {}
+ namespace_id = params[:namespace]
+
+ if namespace_id.present?
+ namespace = Namespace.find_by(id: namespace_id) || Namespace.find_by_path_or_name(namespace_id)
+
+ unless namespace && can?(current_user, :create_projects, namespace)
+ not_found!('Target Namespace')
+ end
+
+ attrs[:namespace] = namespace
+ end
+
@forked_project =
::Projects::ForkService.new(user_project,
- current_user).execute
+ current_user,
+ attrs).execute
+
if @forked_project.errors.any?
conflict!(@forked_project.errors.messages)
else
@@ -228,22 +248,23 @@ module API
# Example Request
# PUT /projects/:id
put ':id' do
- attrs = attributes_for_keys [:name,
- :path,
- :description,
+ attrs = attributes_for_keys [:builds_enabled,
+ :container_registry_enabled,
:default_branch,
+ :description,
:issues_enabled,
+ :lfs_enabled,
:merge_requests_enabled,
- :builds_enabled,
- :wiki_enabled,
- :snippets_enabled,
- :container_registry_enabled,
- :shared_runners_enabled,
+ :name,
+ :only_allow_merge_if_build_succeeds,
+ :path,
:public,
- :visibility_level,
:public_builds,
- :only_allow_merge_if_build_succeeds,
- :lfs_enabled]
+ :request_access_enabled,
+ :shared_runners_enabled,
+ :snippets_enabled,
+ :visibility_level,
+ :wiki_enabled]
attrs = map_public_to_visibility_level(attrs)
authorize_admin_project
authorize! :rename_project, user_project if attrs[:name].present?
@@ -372,23 +393,24 @@ module API
# Share project with group
#
# Parameters:
- # id (required) - The ID of a project
- # group_id (required) - The ID of a group
+ # id (required) - The ID of a project
+ # group_id (required) - The ID of a group
# group_access (required) - Level of permissions for sharing
+ # expires_at (optional) - Share expiration date
#
# Example Request:
# POST /projects/:id/share
post ":id/share" do
authorize! :admin_project, user_project
required_attributes! [:group_id, :group_access]
+ attrs = attributes_for_keys [:group_id, :group_access, :expires_at]
unless user_project.allowed_to_share_with_group?
return render_api_error!("The project sharing with group is disabled", 400)
end
- link = user_project.project_group_links.new
- link.group_id = params[:group_id]
- link.group_access = params[:group_access]
+ link = user_project.project_group_links.new(attrs)
+
if link.save
present link, with: Entities::ProjectGroupLink
else
@@ -414,18 +436,9 @@ module API
# Example Request:
# GET /projects/search/:query
get "/search/:query" do
- ids = current_user.authorized_projects.map(&:id)
- visibility_levels = [ Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC ]
- projects = Project.where("(id in (?) OR visibility_level in (?)) AND (name LIKE (?))", ids, visibility_levels, "%#{params[:query]}%")
- sort = params[:sort] == 'desc' ? 'desc' : 'asc'
-
- projects = case params["order_by"]
- when 'id' then projects.order("id #{sort}")
- when 'name' then projects.order("name #{sort}")
- when 'created_at' then projects.order("created_at #{sort}")
- when 'last_activity_at' then projects.order("last_activity_at #{sort}")
- else projects
- end
+ search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
+ projects = search_service.objects('projects', params[:page])
+ projects = projects.reorder(project_order_by => project_sort)
present paginate(projects), with: Entities::Project
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index c440305ff0f..18c4cad09ae 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -60,6 +60,7 @@ module API
# linkedin - Linkedin
# twitter - Twitter account
# website_url - Website url
+ # organization - Organization
# projects_limit - Number of projects user can create
# extern_uid - External authentication provider UID
# provider - External provider
@@ -74,7 +75,7 @@ module API
post do
authenticated_as_admin!
required_attributes! [:email, :password, :name, :username]
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external, :organization]
admin = attrs.delete(:admin)
confirm = !(attrs.delete(:confirm) =~ /(false|f|no|0)$/i)
user = User.build_user(attrs)
@@ -111,6 +112,7 @@ module API
# linkedin - Linkedin
# twitter - Twitter account
# website_url - Website url
+ # organization - Organization
# projects_limit - Limit projects each user can create
# bio - Bio
# location - Location of the user
@@ -122,7 +124,7 @@ module API
put ":id" do
authenticated_as_admin!
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external, :organization]
user = User.find(params[:id])
not_found!('User') unless user
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 16cd774c81a..affe34394c2 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -64,7 +64,7 @@ module Banzai
end
end
- def project_from_ref_cache(ref)
+ def project_from_ref_cached(ref)
if RequestStore.active?
cache = project_refs_cache
@@ -146,7 +146,7 @@ module Banzai
# 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_cache(project_ref)
+ project = project_from_ref_cached(project_ref)
if project && object = find_object_cached(project, id)
title = object_link_title(object)
@@ -243,11 +243,27 @@ module Banzai
end
end
- # Returns the projects for the given paths.
- def find_projects_for_paths(paths)
+ def projects_relation_for_paths(paths)
Project.where_paths_in(paths).includes(:namespace)
end
+ # Returns projects for the given paths.
+ def find_projects_for_paths(paths)
+ if RequestStore.active?
+ to_query = paths - project_refs_cache.keys
+
+ unless to_query.empty?
+ projects_relation_for_paths(to_query).each do |project|
+ get_or_set_cache(project_refs_cache, project.path_with_namespace) { project }
+ end
+ end
+
+ project_refs_cache.slice(*paths).values
+ else
+ projects_relation_for_paths(paths)
+ end
+ end
+
def current_project_path
@current_project_path ||= project.path_with_namespace
end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 4042e9a4c25..54c5f9a71a4 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -66,7 +66,7 @@ module Banzai
end
end
- def find_projects_for_paths(paths)
+ def projects_relation_for_paths(paths)
super(paths).includes(:gitlab_issue_tracker_service)
end
end
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 6e13282d5f4..2470362e019 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -7,7 +7,7 @@ module Banzai
UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
def whitelist
- whitelist = super.dup
+ whitelist = super
customize_whitelist(whitelist)
@@ -42,58 +42,58 @@ module Banzai
# Allow any protocol in `a` elements...
whitelist[:protocols].delete('a')
- whitelist[:transformers] = whitelist[:transformers].dup
-
# ...but then remove links with unsafe protocols
- whitelist[:transformers].push(remove_unsafe_links)
+ whitelist[:transformers].push(self.class.remove_unsafe_links)
# Remove `rel` attribute from `a` elements
- whitelist[:transformers].push(remove_rel)
+ whitelist[:transformers].push(self.class.remove_rel)
# Remove `class` attribute from non-highlight spans
- whitelist[:transformers].push(clean_spans)
+ whitelist[:transformers].push(self.class.clean_spans)
whitelist
end
- def remove_unsafe_links
- lambda do |env|
- node = env[:node]
+ class << self
+ def remove_unsafe_links
+ lambda do |env|
+ node = env[:node]
- return unless node.name == 'a'
- return unless node.has_attribute?('href')
+ return unless node.name == 'a'
+ return unless node.has_attribute?('href')
- begin
- uri = Addressable::URI.parse(node['href'])
- uri.scheme = uri.scheme.strip.downcase if uri.scheme
+ begin
+ uri = Addressable::URI.parse(node['href'])
+ uri.scheme = uri.scheme.strip.downcase if uri.scheme
- node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
- rescue Addressable::URI::InvalidURIError
- node.remove_attribute('href')
+ node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
+ rescue Addressable::URI::InvalidURIError
+ node.remove_attribute('href')
+ end
end
end
- end
- def remove_rel
- lambda do |env|
- if env[:node_name] == 'a'
- env[:node].remove_attribute('rel')
+ def remove_rel
+ lambda do |env|
+ if env[:node_name] == 'a'
+ env[:node].remove_attribute('rel')
+ end
end
end
- end
- def clean_spans
- lambda do |env|
- node = env[:node]
+ def clean_spans
+ lambda do |env|
+ node = env[:node]
- return unless node.name == 'span'
- return unless node.has_attribute?('class')
+ return unless node.name == 'span'
+ return unless node.has_attribute?('class')
- unless has_ancestor?(node, 'pre')
- node.remove_attribute('class')
- end
+ unless node.ancestors.any? { |n| n.name.casecmp('pre').zero? }
+ node.remove_attribute('class')
+ end
- { node_whitelist: [node] }
+ { node_whitelist: [node] }
+ end
end
end
end
diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb
index 66608c9859c..4efbcaf5c7f 100644
--- a/lib/banzai/filter/task_list_filter.rb
+++ b/lib/banzai/filter/task_list_filter.rb
@@ -10,19 +10,21 @@ module Banzai
# task_list gem.
#
# See https://github.com/github/task_list/pull/60
- class TaskListFilter < TaskList::Filter
- def add_css_class_with_fix(node, *new_class_names)
+ module ClassNamesFilter
+ def add_css_class(node, *new_class_names)
if new_class_names.include?('task-list')
# Don't add class to all lists
return
elsif new_class_names.include?('task-list-item')
- add_css_class_without_fix(node.parent, 'task-list')
+ super(node.parent, 'task-list')
end
- add_css_class_without_fix(node, *new_class_names)
+ super(node, *new_class_names)
end
+ end
- alias_method_chain :add_css_class, :fix
+ class TaskListFilter < TaskList::Filter
+ prepend ClassNamesFilter
end
end
end
diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb
index 2e2c8da311e..e7a1ec8457d 100644
--- a/lib/banzai/filter/wiki_link_filter/rewriter.rb
+++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb
@@ -31,6 +31,7 @@ module Banzai
def apply_relative_link_rules!
if @uri.relative? && @uri.path.present?
link = ::File.join(@wiki_base_path, @uri.path)
+ link = "#{link}##{@uri.fragment}" if @uri.fragment
@uri = Addressable::URI.parse(link)
end
end
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index e8e03e4a98f..f5d110e987b 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -79,7 +79,11 @@ module Banzai
def referenced_by(nodes)
ids = unique_attribute_values(nodes, self.class.data_attribute)
- references_relation.where(id: ids)
+ if ids.empty?
+ references_relation.none
+ else
+ references_relation.where(id: ids)
+ end
end
# Returns the ActiveRecord::Relation to use for querying references in the
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 9f3b582a263..59f85416ee5 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -12,7 +12,7 @@ module Ci
# POST /builds/register
post "register" do
authenticate_runner!
- update_runner_last_contact
+ update_runner_last_contact(save: false)
update_runner_info
required_attributes! [:token]
not_found! unless current_runner.active?
@@ -27,7 +27,7 @@ module Ci
else
Gitlab::Metrics.add_event(:build_not_found)
- not_found!
+ build_not_found!
end
end
@@ -101,6 +101,7 @@ module Ci
# POST /builds/:id/artifacts/authorize
post ":id/artifacts/authorize" do
require_gitlab_workhorse!
+ Gitlab::Workhorse.verify_api_request!(headers)
not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id])
not_found! unless build
@@ -113,7 +114,8 @@ module Ci
end
status 200
- { TempPath: ArtifactUploader.artifacts_upload_path }
+ content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+ Gitlab::Workhorse.artifact_upload_ok
end
# Upload artifacts to build - Runners only
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
index 3f5bdaba3f5..66c05773b68 100644
--- a/lib/ci/api/entities.rb
+++ b/lib/ci/api/entities.rb
@@ -15,6 +15,15 @@ module Ci
expose :filename, :size
end
+ class BuildOptions < Grape::Entity
+ expose :image
+ expose :services
+ expose :artifacts
+ expose :cache
+ expose :dependencies
+ expose :after_script
+ end
+
class Build < Grape::Entity
expose :id, :ref, :tag, :sha, :status
expose :name, :token, :stage
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 199d62d9b8a..23353c62885 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -3,7 +3,7 @@ module Ci
module Helpers
BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN"
BUILD_TOKEN_PARAM = :token
- UPDATE_RUNNER_EVERY = 60
+ UPDATE_RUNNER_EVERY = 40 * 60
def authenticate_runners!
forbidden! unless runner_registration_token_valid?
@@ -14,19 +14,37 @@ module Ci
end
def authenticate_build_token!(build)
- token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s
- forbidden! unless token && build.valid_token?(token)
+ forbidden! unless build_token_valid?(build)
end
def runner_registration_token_valid?
- params[:token] == current_application_settings.runners_registration_token
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(
+ params[:token],
+ current_application_settings.runners_registration_token)
+ end
+
+ def build_token_valid?(build)
+ token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s
+
+ # We require to also check `runners_token` to maintain compatibility with old version of runners
+ token && (build.valid_token?(token) || build.project.valid_runners_token?(token))
end
- def update_runner_last_contact
+ def update_runner_last_contact(save: true)
# Use a random threshold to prevent beating DB updates
+ # it generates a distribution between: [40m, 80m]
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)
+ current_runner.contacted_at = Time.now
+ current_runner.save if current_runner.changed? && save
+ end
+ end
+
+ def build_not_found!
+ if headers['User-Agent'].match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
+ no_content!
+ else
+ not_found!
end
end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 47efd5bd9f2..2fd1fced65c 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -4,7 +4,7 @@ module Ci
include Gitlab::Ci::Config::Node::LegacyValidationHelpers
- attr_reader :path, :cache, :stages
+ attr_reader :path, :cache, :stages, :jobs
def initialize(config, path = nil)
@ci_config = Gitlab::Ci::Config.new(config)
@@ -55,29 +55,36 @@ module Ci
{
stage_idx: @stages.index(job[:stage]),
stage: job[:stage],
- ##
- # Refactoring note:
- # - before script behaves differently than after script
- # - after script returns an array of commands
- # - before script should be a concatenated command
- commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"),
+ commands: job[:commands],
tag_list: job[:tags] || [],
name: job[:name].to_s,
allow_failure: job[:allow_failure] || false,
when: job[:when] || 'on_success',
- environment: job[:environment],
+ environment: job[:environment_name],
yaml_variables: yaml_variables(name),
options: {
- image: job[:image] || @image,
- services: job[:services] || @services,
+ image: job[:image],
+ services: job[:services],
artifacts: job[:artifacts],
- cache: job[:cache] || @cache,
+ cache: job[:cache],
dependencies: job[:dependencies],
- after_script: job[:after_script] || @after_script,
+ after_script: job[:after_script],
+ environment: job[:environment],
}.compact
}
end
+ def self.validation_message(content)
+ return 'Please provide content of .gitlab-ci.yml' if content.blank?
+
+ begin
+ Ci::GitlabCiYamlProcessor.new(content)
+ nil
+ rescue ValidationError, Psych::SyntaxError => e
+ e.message
+ end
+ end
+
private
def initial_parsing
diff --git a/lib/ci/mask_secret.rb b/lib/ci/mask_secret.rb
new file mode 100644
index 00000000000..997377abc55
--- /dev/null
+++ b/lib/ci/mask_secret.rb
@@ -0,0 +1,10 @@
+module Ci::MaskSecret
+ class << self
+ def mask!(value, token)
+ return value unless value.present? && token.present?
+
+ value.gsub!(token, 'x' * token.length)
+ value
+ end
+ end
+end
diff --git a/lib/ci/version_info.rb b/lib/ci/version_info.rb
deleted file mode 100644
index 2a87c91db5e..00000000000
--- a/lib/ci/version_info.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-class VersionInfo
- include Comparable
-
- attr_reader :major, :minor, :patch
-
- def self.parse(str)
- if str && m = str.match(/(\d+)\.(\d+)\.(\d+)/)
- VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i)
- else
- VersionInfo.new
- end
- end
-
- def initialize(major = 0, minor = 0, patch = 0)
- @major = major
- @minor = minor
- @patch = patch
- end
-
- def <=>(other)
- return unless other.is_a? VersionInfo
- return unless valid? && other.valid?
-
- if other.major < @major
- 1
- elsif @major < other.major
- -1
- elsif other.minor < @minor
- 1
- elsif @minor < other.minor
- -1
- elsif other.patch < @patch
- 1
- elsif @patch < other.patch
- -1
- else
- 0
- end
- end
-
- def to_s
- if valid?
- "%d.%d.%d" % [@major, @minor, @patch]
- else
- "Unknown"
- end
- end
-
- def valid?
- @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0
- end
-end
diff --git a/lib/expand_variables.rb b/lib/expand_variables.rb
new file mode 100644
index 00000000000..7b1533d0d32
--- /dev/null
+++ b/lib/expand_variables.rb
@@ -0,0 +1,17 @@
+module ExpandVariables
+ class << self
+ def expand(value, variables)
+ # Convert hash array to variables
+ if variables.is_a?(Array)
+ variables = variables.reduce({}) do |hash, variable|
+ hash[variable[:key]] = variable[:value]
+ hash
+ end
+ end
+
+ value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do
+ variables[$1 || $2]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index a533bac2692..9b484a2ecfd 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -53,6 +53,10 @@ module Gitlab
}
end
+ def sym_options_with_owner
+ sym_options.merge(owner: OWNER)
+ end
+
def protection_options
{
"Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE,
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 91f0270818a..aca5d0020cf 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -1,21 +1,22 @@
module Gitlab
module Auth
- Result = Struct.new(:user, :type)
+ class MissingPersonalTokenError < StandardError; end
class << self
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
- result = Result.new
+ result =
+ service_request_check(login, password, project) ||
+ build_access_token_check(login, password) ||
+ user_with_password_for_git(login, password) ||
+ oauth_access_token_check(login, password) ||
+ lfs_token_check(login, password) ||
+ personal_access_token_check(login, password) ||
+ Gitlab::Auth::Result.new
- if valid_ci_request?(login, password, project)
- result.type = :ci
- else
- result = populate_result(login, password)
- end
+ rate_limit!(ip, success: result.success?, login: login)
- success = result.user.present? || [:ci, :missing_personal_token].include?(result.type)
- rate_limit!(ip, success: success, login: login)
result
end
@@ -57,44 +58,31 @@ module Gitlab
private
- def valid_ci_request?(login, password, project)
+ def service_request_check(login, password, project)
matched_login = /(?<service>^[a-zA-Z]*-ci)-token$/.match(login)
- return false unless project && matched_login.present?
+ return unless project && matched_login.present?
underscored_service = matched_login['service'].underscore
- if underscored_service == 'gitlab_ci'
- project && project.valid_build_token?(password)
- elsif Service.available_services_names.include?(underscored_service)
+ if Service.available_services_names.include?(underscored_service)
# We treat underscored_service as a trusted input because it is included
# in the Service.available_services_names whitelist.
service = project.public_send("#{underscored_service}_service")
- service && service.activated? && service.valid_token?(password)
- end
- end
-
- def populate_result(login, password)
- result =
- user_with_password_for_git(login, password) ||
- oauth_access_token_check(login, password) ||
- personal_access_token_check(login, password)
-
- if result
- result.type = nil unless result.user
-
- if result.user && result.user.two_factor_enabled? && result.type == :gitlab_or_ldap
- result.type = :missing_personal_token
+ if service && service.activated? && service.valid_token?(password)
+ Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)
end
end
-
- result || Result.new
end
def user_with_password_for_git(login, password)
user = find_with_user_password(login, password)
- Result.new(user, :gitlab_or_ldap) if user
+ return unless user
+
+ raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled?
+
+ Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
end
def oauth_access_token_check(login, password)
@@ -102,7 +90,7 @@ module Gitlab
token = Doorkeeper::AccessToken.by_token(password)
if token && token.accessible?
user = User.find_by(id: token.resource_owner_id)
- Result.new(user, :oauth)
+ Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)
end
end
end
@@ -111,9 +99,76 @@ module Gitlab
if login && password
user = User.find_by_personal_access_token(password)
validation = User.by_login(login)
- Result.new(user, :personal_token) if user == validation
+ Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities) if user.present? && user == validation
+ end
+ end
+
+ def lfs_token_check(login, password)
+ deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
+
+ actor =
+ if deploy_key_matches
+ DeployKey.find(deploy_key_matches[1])
+ else
+ User.by_login(login)
+ end
+
+ return unless actor
+
+ token_handler = Gitlab::LfsToken.new(actor)
+
+ authentication_abilities =
+ if token_handler.user?
+ full_authentication_abilities
+ else
+ read_authentication_abilities
+ end
+
+ Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password)
+ end
+
+ def build_access_token_check(login, password)
+ return unless login == 'gitlab-ci-token'
+ return unless password
+
+ build = ::Ci::Build.running.find_by_token(password)
+ return unless build
+ return unless build.project.builds_enabled?
+
+ if build.user
+ # If user is assigned to build, use restricted credentials of user
+ Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities)
+ else
+ # Otherwise use generic CI credentials (backward compatibility)
+ Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities)
end
end
+
+ public
+
+ def build_authentication_abilities
+ [
+ :read_project,
+ :build_download_code,
+ :build_read_container_image,
+ :build_create_container_image
+ ]
+ end
+
+ def read_authentication_abilities
+ [
+ :read_project,
+ :download_code,
+ :read_container_image
+ ]
+ end
+
+ def full_authentication_abilities
+ read_authentication_abilities + [
+ :push_code,
+ :create_container_image
+ ]
+ end
end
end
end
diff --git a/lib/gitlab/auth/result.rb b/lib/gitlab/auth/result.rb
new file mode 100644
index 00000000000..6be7f690676
--- /dev/null
+++ b/lib/gitlab/auth/result.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Auth
+ Result = Struct.new(:actor, :project, :type, :authentication_abilities) do
+ def ci?(for_project)
+ type == :ci &&
+ project &&
+ project == for_project
+ end
+
+ def lfs_deploy_token?(for_project)
+ type == :lfs_deploy_token &&
+ actor &&
+ actor.projects.include?(for_project)
+ end
+
+ def success?
+ actor.present? || type == :ci
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index 839a4fa30d5..79eac66b364 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -6,7 +6,12 @@ module Gitlab
KeyAdder = Struct.new(:io) do
def add_key(id, key)
- key.gsub!(/[[:space:]]+/, ' ').strip!
+ key = Gitlab::Shell.strip_key(key)
+ # Newline and tab are part of the 'protocol' used to transmit id+key to the other end
+ if key.include?("\t") || key.include?("\n")
+ raise Error.new("Invalid key: #{key.inspect}")
+ end
+
io.puts("#{id}\t#{key}")
end
end
@@ -16,6 +21,10 @@ module Gitlab
@version_required ||= File.read(Rails.root.
join('GITLAB_SHELL_VERSION')).strip
end
+
+ def strip_key(key)
+ key.split(/ /)[0, 2].join(' ')
+ end
end
# Init new repository
@@ -107,7 +116,7 @@ module Gitlab
#
def add_key(key_id, key_content)
Gitlab::Utils.system_silent([gitlab_shell_keys_path,
- 'add-key', key_id, key_content])
+ 'add-key', key_id, self.class.strip_key(key_content)])
end
# Batch-add keys to authorized_keys
@@ -195,7 +204,7 @@ module Gitlab
# Create (if necessary) and link the secret token file
def generate_and_link_secret_token
secret_file = Gitlab.config.gitlab_shell.secret_file
- unless File.exist? secret_file
+ unless File.size?(secret_file)
# Generate a new token of 16 random hexadecimal characters and store it in secret_file.
token = SecureRandom.hex(16)
File.write(secret_file, token)
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 7beaecd1cf0..f4b5097adb1 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -21,7 +21,7 @@ module Gitlab
private
- def gl_user_id(project, bitbucket_id)
+ def gitlab_user_id(project, bitbucket_id)
if bitbucket_id
user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
(user && user.id) || project.creator_id
@@ -74,7 +74,7 @@ module Gitlab
description: body,
title: issue["title"],
state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened',
- author_id: gl_user_id(project, reporter)
+ author_id: gitlab_user_id(project, reporter)
)
end
rescue ActiveRecord::RecordInvalid => e
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 4b32eb966aa..cb1065223d4 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -23,6 +23,7 @@ module Gitlab
protected
def protected_branch_checks
+ return unless @branch_name
return unless project.protected_branch?(@branch_name)
if forced_push? && user_access.cannot_do_action?(:force_push_code_to_protected_branches)
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index ae82c0db3f1..bbfa6cf7d05 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -14,7 +14,7 @@ module Gitlab
@config = Loader.new(config).load!
@global = Node::Global.new(@config)
- @global.process!
+ @global.compose!
end
def valid?
diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb
index 2de82d40c9d..6b7ab2fdaf2 100644
--- a/lib/gitlab/ci/config/node/configurable.rb
+++ b/lib/gitlab/ci/config/node/configurable.rb
@@ -23,9 +23,9 @@ module Gitlab
end
end
- private
+ def compose!(deps = nil)
+ return unless valid?
- def compose!
self.class.nodes.each do |key, factory|
factory
.value(@config[key])
@@ -33,6 +33,12 @@ module Gitlab
@entries[key] = factory.create!
end
+
+ yield if block_given?
+
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
end
class_methods do
diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb
index 0c782c422b5..8717eabf81e 100644
--- a/lib/gitlab/ci/config/node/entry.rb
+++ b/lib/gitlab/ci/config/node/entry.rb
@@ -20,11 +20,14 @@ module Gitlab
@validator.validate(:new)
end
- def process!
+ def [](key)
+ @entries[key] || Node::Undefined.new
+ end
+
+ def compose!(deps = nil)
return unless valid?
- compose!
- descendants.each(&:process!)
+ yield if block_given?
end
def leaf?
@@ -73,11 +76,6 @@ module Gitlab
def self.validator
Validator
end
-
- private
-
- def compose!
- end
end
end
end
diff --git a/lib/gitlab/ci/config/node/environment.rb b/lib/gitlab/ci/config/node/environment.rb
new file mode 100644
index 00000000000..d388ab6b879
--- /dev/null
+++ b/lib/gitlab/ci/config/node/environment.rb
@@ -0,0 +1,68 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents an environment.
+ #
+ class Environment < Entry
+ include Validatable
+
+ ALLOWED_KEYS = %i[name url]
+
+ validations do
+ validate do
+ unless hash? || string?
+ errors.add(:config, 'should be a hash or a string')
+ end
+ end
+
+ validates :name, presence: true
+ validates :name,
+ type: {
+ with: String,
+ message: Gitlab::Regex.environment_name_regex_message }
+
+ validates :name,
+ format: {
+ with: Gitlab::Regex.environment_name_regex,
+ message: Gitlab::Regex.environment_name_regex_message }
+
+ with_options if: :hash? do
+ validates :config, allowed_keys: ALLOWED_KEYS
+
+ validates :url,
+ length: { maximum: 255 },
+ addressable_url: true,
+ allow_nil: true
+ end
+ end
+
+ def hash?
+ @config.is_a?(Hash)
+ end
+
+ def string?
+ @config.is_a?(String)
+ end
+
+ def name
+ value[:name]
+ end
+
+ def url
+ value[:url]
+ end
+
+ def value
+ case @config
+ when String then { name: @config }
+ when Hash then @config
+ else {}
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb
index 707b052e6a8..5387f29ad59 100644
--- a/lib/gitlab/ci/config/node/factory.rb
+++ b/lib/gitlab/ci/config/node/factory.rb
@@ -37,8 +37,8 @@ module Gitlab
# See issue #18775.
#
if @value.nil?
- Node::Undefined.new(
- fabricate_undefined
+ Node::Unspecified.new(
+ fabricate_unspecified
)
else
fabricate(@node, @value)
@@ -47,13 +47,13 @@ module Gitlab
private
- def fabricate_undefined
+ def fabricate_unspecified
##
# If node has a default value we fabricate concrete node
# with default value.
#
if @node.default.nil?
- fabricate(Node::Null)
+ fabricate(Node::Undefined)
else
fabricate(@node, @node.default)
end
diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb
index ccd539fb003..2a2943c9288 100644
--- a/lib/gitlab/ci/config/node/global.rb
+++ b/lib/gitlab/ci/config/node/global.rb
@@ -36,15 +36,15 @@ module Gitlab
helpers :before_script, :image, :services, :after_script,
:variables, :stages, :types, :cache, :jobs
- private
-
- def compose!
- super
-
- compose_jobs!
- compose_deprecated_entries!
+ def compose!(_deps = nil)
+ super(self) do
+ compose_jobs!
+ compose_deprecated_entries!
+ end
end
+ private
+
def compose_jobs!
factory = Node::Factory.new(Node::Jobs)
.value(@config.except(*self.class.nodes.keys))
diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb
index e84737acbb9..603334d6793 100644
--- a/lib/gitlab/ci/config/node/job.rb
+++ b/lib/gitlab/ci/config/node/job.rb
@@ -13,7 +13,7 @@ module Gitlab
type stage when artifacts cache dependencies before_script
after_script variables environment]
- attributes :tags, :allow_failure, :when, :environment, :dependencies
+ attributes :tags, :allow_failure, :when, :dependencies
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -29,58 +29,65 @@ module Gitlab
inclusion: { in: %w[on_success on_failure always manual],
message: 'should be on_success, on_failure, ' \
'always or manual' }
- validates :environment,
- type: {
- with: String,
- message: Gitlab::Regex.environment_name_regex_message }
- validates :environment,
- format: {
- with: Gitlab::Regex.environment_name_regex,
- message: Gitlab::Regex.environment_name_regex_message }
validates :dependencies, array_of_strings: true
end
end
- node :before_script, Script,
+ node :before_script, Node::Script,
description: 'Global before script overridden in this job.'
- node :script, Commands,
+ node :script, Node::Commands,
description: 'Commands that will be executed in this job.'
- node :stage, Stage,
+ node :stage, Node::Stage,
description: 'Pipeline stage this job will be executed into.'
- node :type, Stage,
+ node :type, Node::Stage,
description: 'Deprecated: stage this job will be executed into.'
- node :after_script, Script,
+ node :after_script, Node::Script,
description: 'Commands that will be executed when finishing job.'
- node :cache, Cache,
+ node :cache, Node::Cache,
description: 'Cache definition for this job.'
- node :image, Image,
+ node :image, Node::Image,
description: 'Image that will be used to execute this job.'
- node :services, Services,
+ node :services, Node::Services,
description: 'Services that will be used to execute this job.'
- node :only, Trigger,
+ node :only, Node::Trigger,
description: 'Refs policy this job will be executed for.'
- node :except, Trigger,
+ node :except, Node::Trigger,
description: 'Refs policy this job will be executed for.'
- node :variables, Variables,
+ node :variables, Node::Variables,
description: 'Environment variables available for this job.'
- node :artifacts, Artifacts,
+ node :artifacts, Node::Artifacts,
description: 'Artifacts configuration for this job.'
+ node :environment, Node::Environment,
+ description: 'Environment configuration for this job.'
+
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
- :artifacts
+ :artifacts, :commands, :environment
+
+ def compose!(deps = nil)
+ super do
+ if type_defined? && !stage_defined?
+ @entries[:stage] = @entries[:type]
+ end
+
+ @entries.delete(:type)
+ end
+
+ inherit!(deps)
+ end
def name
@metadata[:name]
@@ -90,12 +97,30 @@ module Gitlab
@config.merge(to_hash.compact)
end
+ def commands
+ (before_script_value.to_a + script_value.to_a).join("\n")
+ end
+
private
+ def inherit!(deps)
+ return unless deps
+
+ self.class.nodes.each_key do |key|
+ global_entry = deps[key]
+ job_entry = @entries[key]
+
+ if global_entry.specified? && !job_entry.specified?
+ @entries[key] = global_entry
+ end
+ end
+ end
+
def to_hash
{ name: name,
before_script: before_script,
script: script,
+ commands: commands,
image: image,
services: services,
stage: stage,
@@ -103,19 +128,11 @@ module Gitlab
only: only,
except: except,
variables: variables_defined? ? variables : nil,
+ environment: environment_defined? ? environment : nil,
+ environment_name: environment_defined? ? environment[:name] : nil,
artifacts: artifacts,
after_script: after_script }
end
-
- def compose!
- super
-
- if type_defined? && !stage_defined?
- @entries[:stage] = @entries[:type]
- end
-
- @entries.delete(:type)
- end
end
end
end
diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb
index a1a26d4fd8f..d10e80d1a7d 100644
--- a/lib/gitlab/ci/config/node/jobs.rb
+++ b/lib/gitlab/ci/config/node/jobs.rb
@@ -26,19 +26,23 @@ module Gitlab
name.to_s.start_with?('.')
end
- private
-
- def compose!
- @config.each do |name, config|
- node = hidden?(name) ? Node::Hidden : Node::Job
-
- factory = Node::Factory.new(node)
- .value(config || {})
- .metadata(name: name)
- .with(key: name, parent: self,
- description: "#{name} job definition.")
+ def compose!(deps = nil)
+ super do
+ @config.each do |name, config|
+ node = hidden?(name) ? Node::Hidden : Node::Job
+
+ factory = Node::Factory.new(node)
+ .value(config || {})
+ .metadata(name: name)
+ .with(key: name, parent: self,
+ description: "#{name} job definition.")
+
+ @entries[name] = factory.create!
+ end
- @entries[name] = factory.create!
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/node/null.rb b/lib/gitlab/ci/config/node/null.rb
deleted file mode 100644
index 88a5f53f13c..00000000000
--- a/lib/gitlab/ci/config/node/null.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # This class represents an undefined node.
- #
- # Implements the Null Object pattern.
- #
- class Null < Entry
- def value
- nil
- end
-
- def valid?
- true
- end
-
- def errors
- []
- end
-
- def specified?
- false
- end
-
- def relevant?
- false
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb
index 45fef8c3ae5..33e78023539 100644
--- a/lib/gitlab/ci/config/node/undefined.rb
+++ b/lib/gitlab/ci/config/node/undefined.rb
@@ -3,15 +3,34 @@ module Gitlab
class Config
module Node
##
- # This class represents an unspecified entry node.
+ # This class represents an undefined node.
#
- # It decorates original entry adding method that indicates it is
- # unspecified.
+ # Implements the Null Object pattern.
#
- class Undefined < SimpleDelegator
+ class Undefined < Entry
+ def initialize(*)
+ super(nil)
+ end
+
+ def value
+ nil
+ end
+
+ def valid?
+ true
+ end
+
+ def errors
+ []
+ end
+
def specified?
false
end
+
+ def relevant?
+ false
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/node/unspecified.rb b/lib/gitlab/ci/config/node/unspecified.rb
new file mode 100644
index 00000000000..a7d1f6131b8
--- /dev/null
+++ b/lib/gitlab/ci/config/node/unspecified.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # This class represents an unspecified entry node.
+ #
+ # It decorates original entry adding method that indicates it is
+ # unspecified.
+ #
+ class Unspecified < SimpleDelegator
+ def specified?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline_duration.rb b/lib/gitlab/ci/pipeline_duration.rb
new file mode 100644
index 00000000000..a210e76acaa
--- /dev/null
+++ b/lib/gitlab/ci/pipeline_duration.rb
@@ -0,0 +1,141 @@
+module Gitlab
+ module Ci
+ # # Introduction - total running time
+ #
+ # The problem this module is trying to solve is finding the total running
+ # time amongst all the jobs, excluding retries and pending (queue) time.
+ # We could reduce this problem down to finding the union of periods.
+ #
+ # So each job would be represented as a `Period`, which consists of
+ # `Period#first` as when the job started and `Period#last` as when the
+ # job was finished. A simple example here would be:
+ #
+ # * A (1, 3)
+ # * B (2, 4)
+ # * C (6, 7)
+ #
+ # Here A begins from 1, and ends to 3. B begins from 2, and ends to 4.
+ # C begins from 6, and ends to 7. Visually it could be viewed as:
+ #
+ # 0 1 2 3 4 5 6 7
+ # AAAAAAA
+ # BBBBBBB
+ # CCCC
+ #
+ # The union of A, B, and C would be (1, 4) and (6, 7), therefore the
+ # total running time should be:
+ #
+ # (4 - 1) + (7 - 6) => 4
+ #
+ # # The Algorithm
+ #
+ # The algorithm used here for union would be described as follow.
+ # First we make sure that all periods are sorted by `Period#first`.
+ # Then we try to merge periods by iterating through the first period
+ # to the last period. The goal would be merging all overlapped periods
+ # so that in the end all the periods are discrete. When all periods
+ # are discrete, we're free to just sum all the periods to get real
+ # running time.
+ #
+ # Here we begin from A, and compare it to B. We could find that
+ # before A ends, B already started. That is `B.first <= A.last`
+ # that is `2 <= 3` which means A and B are overlapping!
+ #
+ # When we found that two periods are overlapping, we would need to merge
+ # them into a new period and disregard the old periods. To make a new
+ # period, we take `A.first` as the new first because remember? we sorted
+ # them, so `A.first` must be smaller or equal to `B.first`. And we take
+ # `[A.last, B.last].max` as the new last because we want whoever ended
+ # later. This could be broken into two cases:
+ #
+ # 0 1 2 3 4
+ # AAAAAAA
+ # BBBBBBB
+ #
+ # Or:
+ #
+ # 0 1 2 3 4
+ # AAAAAAAAAA
+ # BBBB
+ #
+ # So that we need to take whoever ends later. Back to our example,
+ # after merging and discard A and B it could be visually viewed as:
+ #
+ # 0 1 2 3 4 5 6 7
+ # DDDDDDDDDD
+ # CCCC
+ #
+ # Now we could go on and compare the newly created D and the old C.
+ # We could figure out that D and C are not overlapping by checking
+ # `C.first <= D.last` is `false`. Therefore we need to keep both C
+ # and D. The example would end here because there are no more jobs.
+ #
+ # After having the union of all periods, we just need to sum the length
+ # of all periods to get total time.
+ #
+ # (4 - 1) + (7 - 6) => 4
+ #
+ # That is 4 is the answer in the example.
+ module PipelineDuration
+ extend self
+
+ Period = Struct.new(:first, :last) do
+ def duration
+ last - first
+ end
+ end
+
+ def from_pipeline(pipeline)
+ status = %w[success failed running canceled]
+ builds = pipeline.builds.latest.
+ where(status: status).where.not(started_at: nil).order(:started_at)
+
+ from_builds(builds)
+ end
+
+ def from_builds(builds)
+ now = Time.now
+
+ periods = builds.map do |b|
+ Period.new(b.started_at, b.finished_at || now)
+ end
+
+ from_periods(periods)
+ end
+
+ # periods should be sorted by `first`
+ def from_periods(periods)
+ process_duration(process_periods(periods))
+ end
+
+ private
+
+ def process_periods(periods)
+ return periods if periods.empty?
+
+ periods.drop(1).inject([periods.first]) do |result, current|
+ previous = result.last
+
+ if overlap?(previous, current)
+ result[-1] = merge(previous, current)
+ result
+ else
+ result << current
+ end
+ end
+ end
+
+ def overlap?(previous, current)
+ current.first <= previous.last
+ end
+
+ def merge(previous, current)
+ Period.new(previous.first, [previous.last, current.last].max)
+ end
+
+ def process_duration(periods)
+ periods.sum(&:duration)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
index 2d4d55daeeb..98e842cded3 100644
--- a/lib/gitlab/conflict/parser.rb
+++ b/lib/gitlab/conflict/parser.rb
@@ -18,7 +18,7 @@ module Gitlab
def parse(text, our_path:, their_path:, parent_file: nil)
raise UnmergeableFile if text.blank? # Typically a binary file
- raise UnmergeableFile if text.length > 102400
+ raise UnmergeableFile if text.length > 200.kilobytes
begin
text.to_json
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index bd681f03173..b164f5a2eea 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -1,16 +1,16 @@
module Gitlab
class ContributionsCalendar
- attr_reader :timestamps, :projects, :user
+ attr_reader :activity_dates, :projects, :user
def initialize(projects, user)
@projects = projects
@user = user
end
- def timestamps
- return @timestamps if @timestamps.present?
+ def activity_dates
+ return @activity_dates if @activity_dates.present?
- @timestamps = {}
+ @activity_dates = {}
date_from = 1.year.ago
events = Event.reorder(nil).contributions.where(author_id: user.id).
@@ -19,18 +19,17 @@ module Gitlab
select('date(created_at) as date, count(id) as total_amount').
map(&:attributes)
- dates = (1.year.ago.to_date..Date.today).to_a
+ activity_dates = (1.year.ago.to_date..Date.today).to_a
- dates.each do |date|
- date_id = date.to_time.to_i.to_s
+ activity_dates.each do |date|
day_events = events.find { |day_events| day_events["date"] == date }
if day_events
- @timestamps[date_id] = day_events["total_amount"]
+ @activity_dates[date] = day_events["total_amount"]
end
end
- @timestamps
+ @activity_dates
end
def events_by_date(date)
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 12fbb78c53e..ef9160d6437 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -59,10 +59,8 @@ module Gitlab
# When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
active_db_connection = ActiveRecord::Base.connection.active? rescue false
- ENV['USE_DB'] != 'false' &&
active_db_connection &&
- ActiveRecord::Base.connection.table_exists?('application_settings')
-
+ ActiveRecord::Base.connection.table_exists?('application_settings')
rescue ActiveRecord::NoDatabaseError
false
end
diff --git a/lib/gitlab/database/date_time.rb b/lib/gitlab/database/date_time.rb
new file mode 100644
index 00000000000..b6a89f715fd
--- /dev/null
+++ b/lib/gitlab/database/date_time.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Database
+ module DateTime
+ # Find the first of the `end_time_attrs` that isn't `NULL`. Subtract from it
+ # the first of the `start_time_attrs` that isn't NULL. `SELECT` the resulting interval
+ # along with an alias specified by the `as` parameter.
+ #
+ # Note: For MySQL, the interval is returned in seconds.
+ # For PostgreSQL, the interval is returned as an INTERVAL type.
+ def subtract_datetimes(query_so_far, end_time_attrs, start_time_attrs, as)
+ diff_fn = if Gitlab::Database.postgresql?
+ Arel::Nodes::Subtraction.new(
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs)),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)))
+ elsif Gitlab::Database.mysql?
+ Arel::Nodes::NamedFunction.new(
+ "TIMESTAMPDIFF",
+ [Arel.sql('second'),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs))])
+ end
+
+ query_so_far.project(diff_fn.as(as))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
new file mode 100644
index 00000000000..1444d25ebc7
--- /dev/null
+++ b/lib/gitlab/database/median.rb
@@ -0,0 +1,112 @@
+# https://www.periscopedata.com/blog/medians-in-sql.html
+module Gitlab
+ module Database
+ module Median
+ def median_datetime(arel_table, query_so_far, column_sym)
+ median_queries =
+ if Gitlab::Database.postgresql?
+ pg_median_datetime_sql(arel_table, query_so_far, column_sym)
+ elsif Gitlab::Database.mysql?
+ mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
+ end
+
+ results = Array.wrap(median_queries).map do |query|
+ ActiveRecord::Base.connection.execute(query)
+ end
+ extract_median(results).presence
+ end
+
+ def extract_median(results)
+ result = results.compact.first
+
+ if Gitlab::Database.postgresql?
+ result = result.first.presence
+ median = result['median'] if result
+ median.to_f if median
+ elsif Gitlab::Database.mysql?
+ result.to_a.flatten.first
+ end
+ end
+
+ def mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
+ query = arel_table.
+ from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name)).
+ project(average([arel_table[column_sym]], 'median')).
+ where(
+ Arel::Nodes::Between.new(
+ Arel.sql("(select @row_id := @row_id + 1)"),
+ Arel::Nodes::And.new(
+ [Arel.sql('@ct/2.0'),
+ Arel.sql('@ct/2.0 + 1')]
+ )
+ )
+ ).
+ # Disallow negative values
+ where(arel_table[column_sym].gteq(0))
+
+ [
+ Arel.sql("CREATE TEMPORARY TABLE IF NOT EXISTS #{query_so_far.to_sql}"),
+ Arel.sql("set @ct := (select count(1) from #{arel_table.table_name});"),
+ Arel.sql("set @row_id := 0;"),
+ query.to_sql,
+ Arel.sql("DROP TEMPORARY TABLE IF EXISTS #{arel_table.table_name};")
+ ]
+ end
+
+ def pg_median_datetime_sql(arel_table, query_so_far, column_sym)
+ # Create a CTE with the column we're operating on, row number (after sorting by the column
+ # we're operating on), and count of the table we're operating on (duplicated across) all rows
+ # of the CTE. For example, if we're looking to find the median of the `projects.star_count`
+ # column, the CTE might look like this:
+ #
+ # star_count | row_id | ct
+ # ------------+--------+----
+ # 5 | 1 | 3
+ # 9 | 2 | 3
+ # 15 | 3 | 3
+ cte_table = Arel::Table.new("ordered_records")
+ cte = Arel::Nodes::As.new(
+ cte_table,
+ arel_table.
+ project(
+ arel_table[column_sym].as(column_sym.to_s),
+ Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("row_number", []),
+ Arel::Nodes::Window.new.order(arel_table[column_sym])).as('row_id'),
+ arel_table.project("COUNT(1)").as('ct')).
+ # Disallow negative values
+ where(arel_table[column_sym].gteq(zero_interval)))
+
+ # From the CTE, select either the middle row or the middle two rows (this is accomplished
+ # by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the
+ # selected rows, and this is the median value.
+ cte_table.project(average([extract_epoch(cte_table[column_sym])], "median")).
+ where(
+ Arel::Nodes::Between.new(
+ cte_table[:row_id],
+ Arel::Nodes::And.new(
+ [(cte_table[:ct] / Arel.sql('2.0')),
+ (cte_table[:ct] / Arel.sql('2.0') + 1)]
+ )
+ )
+ ).
+ with(query_so_far, cte).
+ to_sql
+ end
+
+ private
+
+ def average(args, as)
+ Arel::Nodes::NamedFunction.new("AVG", args, as)
+ end
+
+ def extract_epoch(arel_attribute)
+ Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
+ end
+
+ # Need to cast '0' to an INTERVAL before we can check if the interval is positive
+ def zero_interval
+ Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 927f9dad20b..0bd6e148ba8 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -129,12 +129,14 @@ module Gitlab
# column - The name of the column to add.
# type - The column type (e.g. `:integer`).
# default - The default value for the column.
+ # limit - Sets a column limit. For example, for :integer, the default is
+ # 4-bytes. Set `limit: 8` to allow 8-byte integers.
# allow_null - When set to `true` the column will allow NULL values, the
# default is to not allow NULL values.
#
# This method can also take a block which is passed directly to the
# `update_column_in_batches` method.
- def add_column_with_default(table, column, type, default:, allow_null: false, &block)
+ def add_column_with_default(table, column, type, default:, limit: nil, allow_null: false, &block)
if transaction_open?
raise 'add_column_with_default can not be run inside a transaction, ' \
'you can disable transactions by calling disable_ddl_transaction! ' \
@@ -144,7 +146,11 @@ module Gitlab
disable_statement_timeout
transaction do
- add_column(table, column, type, default: nil)
+ if limit
+ add_column(table, column, type, default: nil, limit: limit)
+ else
+ add_column(table, column, type, default: nil)
+ end
# Changing the default before the update ensures any newly inserted
# rows already use the proper default value.
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 7584efe4fa8..3cd515e4a3a 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -18,6 +18,16 @@ module Gitlab
end
end
+ def committer_hash(email:, name:)
+ return if email.nil? || name.nil?
+
+ {
+ email: email,
+ name: name,
+ time: Time.now
+ }
+ end
+
def tag_name(ref)
ref = ref.to_s
if self.tag_ref?(ref)
diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb
index 9b681e636c7..bd90d24a2ec 100644
--- a/lib/gitlab/git/hook.rb
+++ b/lib/gitlab/git/hook.rb
@@ -17,11 +17,13 @@ module Gitlab
def trigger(gl_id, oldrev, newrev, ref)
return [true, nil] 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)
+ Bundler.with_clean_env do
+ 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
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 1882eb8d050..799794c0171 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -5,12 +5,13 @@ module Gitlab
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
PUSH_COMMANDS = %w{ git-receive-pack }
- attr_reader :actor, :project, :protocol, :user_access
+ attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
- def initialize(actor, project, protocol)
+ def initialize(actor, project, protocol, authentication_abilities:)
@actor = actor
@project = project
@protocol = protocol
+ @authentication_abilities = authentication_abilities
@user_access = UserAccess.new(user, project: project)
end
@@ -60,14 +61,26 @@ module Gitlab
end
def user_download_access_check
- unless user_access.can_do_action?(:download_code)
+ unless user_can_download_code? || build_can_download_code?
return build_status_object(false, "You are not allowed to download code from this project.")
end
build_status_object(true)
end
+ def user_can_download_code?
+ authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code)
+ end
+
+ def build_can_download_code?
+ authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
+ end
+
def user_push_access_check(changes)
+ unless authentication_abilities.include?(:push_code)
+ return build_status_object(false, "You are not allowed to upload code for this project.")
+ end
+
if changes.blank?
return build_status_object(true)
end
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
index 72992baffd4..8cacf4f4925 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -15,11 +15,16 @@ module Gitlab
private
- def gl_user_id(github_id)
+ def gitlab_user_id(github_id)
User.joins(:identities).
find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s).
try(:id)
end
+
+ def gitlab_author_id
+ return @gitlab_author_id if defined?(@gitlab_author_id)
+ @gitlab_author_id = gitlab_user_id(raw_data.user.id)
+ end
end
end
end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 084e514492c..e33ac61f5ae 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -52,7 +52,7 @@ module Gitlab
def method_missing(method, *args, &block)
if api.respond_to?(method)
- request { api.send(method, *args, &block) }
+ request(method, *args, &block)
else
super(method, *args, &block)
end
@@ -99,20 +99,19 @@ module Gitlab
rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME
end
- def request
+ def request(method, *args, &block)
sleep rate_limit_sleep_time if rate_limit_exceed?
- data = yield
+ data = api.send(method, *args, &block)
+ yield data
last_response = api.last_response
while last_response.rels[:next]
sleep rate_limit_sleep_time if rate_limit_exceed?
last_response = last_response.rels[:next].get
- data.concat(last_response.data) if last_response.data.is_a?(Array)
+ yield last_response.data if last_response.data.is_a?(Array)
end
-
- data
end
end
end
diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb
index 2c1b94ef2cd..2bddcde2b7c 100644
--- a/lib/gitlab/github_import/comment_formatter.rb
+++ b/lib/gitlab/github_import/comment_formatter.rb
@@ -21,7 +21,7 @@ module Gitlab
end
def author_id
- gl_user_id(raw_data.user.id) || project.creator_id
+ gitlab_author_id || project.creator_id
end
def body
@@ -52,7 +52,11 @@ module Gitlab
end
def note
- formatter.author_line(author) + body
+ if gitlab_author_id
+ body
+ else
+ formatter.author_line(author) + body
+ end
end
def type
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 4fdc2f46be0..b8321244473 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -10,6 +10,7 @@ module Gitlab
@repo = project.import_source
@repo_url = project.import_url
@errors = []
+ @labels = {}
if credentials
@client = Client.new(credentials[:user])
@@ -23,7 +24,9 @@ module Gitlab
import_milestones
import_issues
import_pull_requests
+ import_comments
import_wiki
+ import_releases
handle_errors
true
@@ -45,66 +48,68 @@ module Gitlab
end
def import_labels
- labels = client.labels(repo, per_page: 100)
-
- labels.each do |raw|
- begin
- LabelFormatter.new(project, raw).create!
- rescue => e
- errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ client.labels(repo, per_page: 100) do |labels|
+ labels.each do |raw|
+ begin
+ label = LabelFormatter.new(project, raw).create!
+ @labels[label.title] = label.id
+ rescue => e
+ errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
end
end
end
def import_milestones
- milestones = client.milestones(repo, state: :all, per_page: 100)
-
- milestones.each do |raw|
- begin
- MilestoneFormatter.new(project, raw).create!
- rescue => e
- errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ client.milestones(repo, state: :all, per_page: 100) do |milestones|
+ milestones.each do |raw|
+ begin
+ MilestoneFormatter.new(project, raw).create!
+ rescue => e
+ errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
end
end
end
def import_issues
- issues = client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100)
-
- issues.each do |raw|
- gh_issue = IssueFormatter.new(project, raw)
-
- if gh_issue.valid?
- begin
- issue = gh_issue.create!
- apply_labels(issue)
- import_comments(issue) if gh_issue.has_comments?
- rescue => e
- errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
+ issues.each do |raw|
+ gh_issue = IssueFormatter.new(project, raw)
+
+ if gh_issue.valid?
+ begin
+ issue = gh_issue.create!
+ apply_labels(issue, raw)
+ rescue => e
+ errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
end
end
end
end
def import_pull_requests
- pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100)
- pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?)
-
- pull_requests.each do |pull_request|
- begin
- restore_source_branch(pull_request) unless pull_request.source_branch_exists?
- restore_target_branch(pull_request) unless pull_request.target_branch_exists?
-
- merge_request = pull_request.create!
- apply_labels(merge_request)
- import_comments(merge_request)
- import_comments_on_diff(merge_request)
- rescue => e
- errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message }
- ensure
- clean_up_restored_branches(pull_request)
+ client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
+ pull_requests.each do |raw|
+ pull_request = PullRequestFormatter.new(project, raw)
+ next unless pull_request.valid?
+
+ begin
+ restore_source_branch(pull_request) unless pull_request.source_branch_exists?
+ restore_target_branch(pull_request) unless pull_request.target_branch_exists?
+
+ merge_request = pull_request.create!
+ apply_labels(merge_request, raw)
+ rescue => e
+ errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message }
+ ensure
+ clean_up_restored_branches(pull_request)
+ end
end
end
+
+ project.repository.after_remove_branch
end
def restore_source_branch(pull_request)
@@ -124,38 +129,38 @@ module Gitlab
def clean_up_restored_branches(pull_request)
remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists?
remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
-
- project.repository.after_remove_branch
end
- def apply_labels(issuable)
- issue = client.issue(repo, issuable.iid)
-
- if issue.labels.count > 0
- label_ids = issue.labels
- .map { |raw| LabelFormatter.new(project, raw).attributes }
- .map { |attrs| Label.find_by(attrs).try(:id) }
+ def apply_labels(issuable, raw_issuable)
+ if raw_issuable.labels.count > 0
+ label_ids = raw_issuable.labels
+ .map { |attrs| @labels[attrs.name] }
.compact
issuable.update_attribute(:label_ids, label_ids)
end
end
- def import_comments(issuable)
- comments = client.issue_comments(repo, issuable.iid, per_page: 100)
- create_comments(issuable, comments)
- end
+ def import_comments
+ client.issues_comments(repo, per_page: 100) do |comments|
+ create_comments(comments, :issue)
+ end
- def import_comments_on_diff(merge_request)
- comments = client.pull_request_comments(repo, merge_request.iid, per_page: 100)
- create_comments(merge_request, comments)
+ client.pull_requests_comments(repo, per_page: 100) do |comments|
+ create_comments(comments, :pull_request)
+ end
end
- def create_comments(issuable, comments)
+ def create_comments(comments, issuable_type)
ActiveRecord::Base.no_touching do
comments.each do |raw|
begin
- comment = CommentFormatter.new(project, raw)
+ comment = CommentFormatter.new(project, raw)
+ issuable_class = issuable_type == :issue ? Issue : MergeRequest
+ iid = raw.send("#{issuable_type}_url").split('/').last # GH doesn't return parent ID directly
+ issuable = issuable_class.find_by_iid(iid)
+ next unless issuable
+
issuable.notes.create!(comment.attributes)
rescue => e
errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
@@ -178,6 +183,19 @@ module Gitlab
errors << { type: :wiki, errors: e.message }
end
end
+
+ def import_releases
+ client.releases(repo, per_page: 100) do |releases|
+ releases.each do |raw|
+ begin
+ gh_release = ReleaseFormatter.new(project, raw)
+ gh_release.create! if gh_release.valid?
+ rescue => e
+ errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
+ end
+ end
+ end
end
end
end
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index 07edbe37a13..77621de9f4c 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -40,7 +40,7 @@ module Gitlab
def assignee_id
if assigned?
- gl_user_id(raw_data.assignee.id)
+ gitlab_user_id(raw_data.assignee.id)
end
end
@@ -49,7 +49,7 @@ module Gitlab
end
def author_id
- gl_user_id(raw_data.user.id) || project.creator_id
+ gitlab_author_id || project.creator_id
end
def body
@@ -57,7 +57,11 @@ module Gitlab
end
def description
- @formatter.author_line(author) + body
+ if gitlab_author_id
+ body
+ else
+ formatter.author_line(author) + body
+ end
end
def milestone
diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb
index 9f18244e7d7..2cad7fca88e 100644
--- a/lib/gitlab/github_import/label_formatter.rb
+++ b/lib/gitlab/github_import/label_formatter.rb
@@ -13,6 +13,12 @@ module Gitlab
Label
end
+ def create!
+ project.labels.find_or_create_by!(title: title) do |label|
+ label.color = color
+ end
+ end
+
private
def color
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index e9725880c5e..605abfabdab 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -3,8 +3,9 @@ module Gitlab
class ProjectCreator
attr_reader :repo, :namespace, :current_user, :session_data
- def initialize(repo, namespace, current_user, session_data)
+ def initialize(repo, name, namespace, current_user, session_data)
@repo = repo
+ @name = name
@namespace = namespace
@current_user = current_user
@session_data = session_data
@@ -13,8 +14,8 @@ module Gitlab
def execute
project = ::Projects::CreateService.new(
current_user,
- name: repo.name,
- path: repo.name,
+ name: @name,
+ path: @name,
description: repo.description,
namespace_id: namespace.id,
visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility,
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index d9d436d7490..1408683100f 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -68,7 +68,7 @@ module Gitlab
def assignee_id
if assigned?
- gl_user_id(raw_data.assignee.id)
+ gitlab_user_id(raw_data.assignee.id)
end
end
@@ -77,7 +77,7 @@ module Gitlab
end
def author_id
- gl_user_id(raw_data.user.id) || project.creator_id
+ gitlab_author_id || project.creator_id
end
def body
@@ -85,7 +85,11 @@ module Gitlab
end
def description
- formatter.author_line(author) + body
+ if gitlab_author_id
+ body
+ else
+ formatter.author_line(author) + body
+ end
end
def milestone
diff --git a/lib/gitlab/github_import/release_formatter.rb b/lib/gitlab/github_import/release_formatter.rb
new file mode 100644
index 00000000000..73d643b00ad
--- /dev/null
+++ b/lib/gitlab/github_import/release_formatter.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module GithubImport
+ class ReleaseFormatter < BaseFormatter
+ def attributes
+ {
+ project: project,
+ tag: raw_data.tag_name,
+ description: raw_data.body,
+ created_at: raw_data.created_at,
+ updated_at: raw_data.created_at
+ }
+ end
+
+ def klass
+ Release
+ end
+
+ def valid?
+ !raw_data.draft
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb
index 46d40f75be6..e44d7934fda 100644
--- a/lib/gitlab/gitlab_import/importer.rb
+++ b/lib/gitlab/gitlab_import/importer.rb
@@ -41,7 +41,8 @@ module Gitlab
title: issue["title"],
state: issue["state"],
updated_at: issue["updated_at"],
- author_id: gl_user_id(project, issue["author"]["id"])
+ author_id: gitlab_user_id(project, issue["author"]["id"]),
+ confidential: issue["confidential"]
)
end
end
@@ -51,7 +52,7 @@ module Gitlab
private
- def gl_user_id(project, gitlab_id)
+ def gitlab_user_id(project, gitlab_id)
user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'gitlab'", gitlab_id.to_s)
(user && user.id) || project.creator_id
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index c5a11148d33..2c21804fe7a 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -11,7 +11,6 @@ module Gitlab
if current_user
gon.current_user_id = current_user.id
- gon.api_token = current_user.private_token
end
end
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index bb562bdcd2c..181e288a014 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -2,7 +2,8 @@ module Gitlab
module ImportExport
extend self
- VERSION = '0.1.3'
+ # For every version update, the version history in import_export.md has to be kept up to date.
+ VERSION = '0.1.4'
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index c2e8a1ca5dd..bb9d1080330 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -1,5 +1,8 @@
# Model relationships to be included in the project import/export
project_tree:
+ - :labels
+ - milestones:
+ - :events
- issues:
- :events
- notes:
@@ -10,6 +13,7 @@ project_tree:
- milestone:
- :events
- snippets:
+ - :award_emoji
- notes:
:author
- :releases
@@ -35,10 +39,9 @@ project_tree:
- :deploy_keys
- :services
- :hooks
- - :protected_branches
- - :labels
- - milestones:
- - :events
+ - protected_branches:
+ - :merge_access_levels
+ - :push_access_levels
- :project_feature
# Only include the following attributes for the models specified.
@@ -64,9 +67,13 @@ excluded_attributes:
- :milestone_id
merge_requests:
- :milestone_id
+ award_emoji:
+ - :awardable_id
methods:
statuses:
- :type
+ services:
+ - :type
merge_request_diff:
- :utf8_st_diffs
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index c7b3551b84c..35ff134ea19 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -61,11 +61,17 @@ module Gitlab
def restore_project
return @project unless @tree_hash
- project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) }
@project.update(project_params)
@project
end
+ def project_params
+ @tree_hash.reject do |key, value|
+ # return params that are not 1 to many or 1 to 1 relations
+ value.is_a?(Array) || key == key.singularize
+ end
+ end
+
# Given a relation hash containing one or more models and its relationships,
# loops through each model and each object from a model type and
# and assigns its correspondent attributes hash from +tree_hash+
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index b0726268ca6..354ccd64696 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -7,7 +7,9 @@ module Gitlab
variables: 'Ci::Variable',
triggers: 'Ci::Trigger',
builds: 'Ci::Build',
- hooks: 'ProjectHook' }.freeze
+ hooks: 'ProjectHook',
+ merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
+ push_access_levels: 'ProtectedBranch::PushAccessLevel' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
@@ -17,6 +19,8 @@ module Gitlab
EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze
+ FINDER_ATTRIBUTES = %w[title project_id].freeze
+
def self.create(*args)
new(*args).create
end
@@ -149,7 +153,7 @@ module Gitlab
end
def parsed_relation_hash
- @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
+ @parsed_relation_hash ||= @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
end
def set_st_diffs
@@ -161,14 +165,30 @@ module Gitlab
# Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin
if EXISTING_OBJECT_CHECK.include?(@relation_name)
- existing_object = relation_class.find_or_initialize_by(parsed_relation_hash.slice('title', 'project_id'))
- existing_object.assign_attributes(parsed_relation_hash)
+ events = parsed_relation_hash.delete('events')
+
+ unless events.blank?
+ existing_object.assign_attributes(events: events)
+ end
+
existing_object
else
relation_class.new(parsed_relation_hash)
end
end
end
+
+ def existing_object
+ @existing_object ||=
+ begin
+ finder_hash = parsed_relation_hash.slice(*FINDER_ATTRIBUTES)
+ existing_object = relation_class.find_or_create_by(finder_hash)
+ # Done in two steps, as MySQL behaves differently than PostgreSQL using
+ # the +find_or_create_by+ method and does not return the ID the second time.
+ existing_object.update(parsed_relation_hash)
+ existing_object
+ end
+ end
end
end
end
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 6d9379acf25..d1e33ea8678 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -22,10 +22,6 @@ module Gitlab
private
- def repos_path
- Gitlab.config.gitlab_shell.repos_path
- end
-
def path_to_repo
@project.repository.path_to_repo
end
diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
index de3fe6d822e..fc08082fc86 100644
--- a/lib/gitlab/import_export/version_checker.rb
+++ b/lib/gitlab/import_export/version_checker.rb
@@ -24,8 +24,8 @@ module Gitlab
end
def verify_version!(version)
- if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.version)
- raise Gitlab::ImportExport::Error.new("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
+ if Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
+ raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
else
true
end
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index 2f326d00a2f..7e06bd2b0fb 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -51,8 +51,6 @@ module Gitlab
user.ldap_block
false
end
- rescue
- false
end
def adapter
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index 9a5bcfb5c9b..8b38cfaefb6 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -23,31 +23,7 @@ module Gitlab
end
def users(field, value, limit = nil)
- if field.to_sym == :dn
- options = {
- base: value,
- scope: Net::LDAP::SearchScope_BaseObject
- }
- else
- options = {
- base: config.base,
- filter: Net::LDAP::Filter.eq(field, value)
- }
- end
-
- if config.user_filter.present?
- user_filter = Net::LDAP::Filter.construct(config.user_filter)
-
- options[:filter] = if options[:filter]
- Net::LDAP::Filter.join(options[:filter], user_filter)
- else
- user_filter
- end
- end
-
- if limit.present?
- options.merge!(size: limit)
- end
+ options = user_options(field, value, limit)
entries = ldap_search(options).select do |entry|
entry.respond_to? config.uid
@@ -86,10 +62,49 @@ module Gitlab
results
end
end
+ rescue Net::LDAP::Error => error
+ Rails.logger.warn("LDAP search raised exception #{error.class}: #{error.message}")
+ []
rescue Timeout::Error
Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds")
[]
end
+
+ private
+
+ def user_options(field, value, limit)
+ options = { attributes: user_attributes }
+ options[:size] = limit if limit
+
+ if field.to_sym == :dn
+ options[:base] = value
+ options[:scope] = Net::LDAP::SearchScope_BaseObject
+ options[:filter] = user_filter
+ else
+ options[:base] = config.base
+ options[:filter] = user_filter(Net::LDAP::Filter.eq(field, value))
+ end
+
+ options
+ end
+
+ def user_filter(filter = nil)
+ if config.user_filter.present?
+ user_filter = Net::LDAP::Filter.construct(config.user_filter)
+ end
+
+ if user_filter && filter
+ Net::LDAP::Filter.join(filter, user_filter)
+ elsif user_filter
+ user_filter
+ else
+ filter
+ end
+ end
+
+ def user_attributes
+ %W(#{config.uid} cn mail dn)
+ end
end
end
end
diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb
new file mode 100644
index 00000000000..5f67e97fa2a
--- /dev/null
+++ b/lib/gitlab/lfs_token.rb
@@ -0,0 +1,48 @@
+module Gitlab
+ class LfsToken
+ attr_accessor :actor
+
+ TOKEN_LENGTH = 50
+ EXPIRY_TIME = 1800
+
+ def initialize(actor)
+ @actor =
+ case actor
+ when DeployKey, User
+ actor
+ when Key
+ actor.user
+ else
+ raise 'Bad Actor'
+ end
+ end
+
+ def token
+ Gitlab::Redis.with do |redis|
+ token = redis.get(redis_key)
+ token ||= Devise.friendly_token(TOKEN_LENGTH)
+ redis.set(redis_key, token, ex: EXPIRY_TIME)
+
+ token
+ end
+ end
+
+ def user?
+ actor.is_a?(User)
+ end
+
+ def type
+ actor.is_a?(User) ? :lfs_token : :lfs_deploy_token
+ end
+
+ def actor_name
+ actor.is_a?(User) ? actor.username : "lfs+deploy-key-#{actor.id}"
+ end
+
+ private
+
+ def redis_key
+ "gitlab:lfs_token:#{actor.class.name.underscore}_#{actor.id}" if actor
+ end
+ end
+end
diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb
index ca23ccef25b..cc74bb29087 100644
--- a/lib/gitlab/popen.rb
+++ b/lib/gitlab/popen.rb
@@ -18,18 +18,18 @@ module Gitlab
FileUtils.mkdir_p(path)
end
- @cmd_output = ""
- @cmd_status = 0
+ cmd_output = ""
+ cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
- # We are not using stdin so we should close it, in case the command we
- # are running waits for input.
+ yield(stdin) if block_given?
stdin.close
- @cmd_output << stdout.read
- @cmd_output << stderr.read
- @cmd_status = wait_thr.value.exitstatus
+
+ cmd_output << stdout.read
+ cmd_output << stderr.read
+ cmd_status = wait_thr.value.exitstatus
end
- [@cmd_output, @cmd_status]
+ [cmd_output, cmd_status]
end
end
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 183bd10d6a3..5b9cfaeb2f8 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -28,11 +28,6 @@ module Gitlab
end
end
- def total_count
- @total_count ||= issues_count + merge_requests_count + blobs_count +
- notes_count + wiki_blobs_count + commits_count
- end
-
def blobs_count
@blobs_count ||= blobs.count
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 9376b54f43b..3faab937726 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -9,35 +9,35 @@ module Gitlab
SIDEKIQ_NAMESPACE = 'resque:gitlab'
MAILROOM_NAMESPACE = 'mail_room:gitlab'
DEFAULT_REDIS_URL = 'redis://localhost:6379'
-
- # To be thread-safe we must be careful when writing the class instance
- # variables @url and @pool. Because @pool depends on @url we need two
- # mutexes to prevent deadlock.
- PARAMS_MUTEX = Mutex.new
- POOL_MUTEX = Mutex.new
- private_constant :PARAMS_MUTEX, :POOL_MUTEX
+ CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__)
class << self
+ # Do NOT cache in an instance variable. Result may be mutated by caller.
def params
- @params || PARAMS_MUTEX.synchronize { @params = new.params }
+ new.params
end
+ # Do NOT cache in an instance variable. Result may be mutated by caller.
# @deprecated Use .params instead to get sentinel support
def url
new.url
end
def with
- if @pool.nil?
- POOL_MUTEX.synchronize do
- @pool = ConnectionPool.new { ::Redis.new(params) }
- end
- end
+ @pool ||= ConnectionPool.new { ::Redis.new(params) }
@pool.with { |redis| yield redis }
end
- def reset_params!
- @params = nil
+ def _raw_config
+ return @_raw_config if defined?(@_raw_config)
+
+ begin
+ @_raw_config = File.read(CONFIG_FILE).freeze
+ rescue Errno::ENOENT
+ @_raw_config = false
+ end
+
+ @_raw_config
end
end
@@ -83,12 +83,7 @@ module Gitlab
end
def fetch_config
- file = config_file
- File.exist?(file) ? YAML.load_file(file)[@rails_env] : false
- end
-
- def config_file
- File.expand_path('../../../config/resque.yml', __FILE__)
+ self.class._raw_config ? YAML.load(self.class._raw_config)[@rails_env] : false
end
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index ffad5e17c78..776bbcbb5d0 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -44,7 +44,7 @@ module Gitlab
end
def file_name_regex_message
- "can contain only letters, digits, '_', '-', '@' and '.'. "
+ "can contain only letters, digits, '_', '-', '@' and '.'."
end
def file_path_regex
@@ -52,7 +52,7 @@ module Gitlab
end
def file_path_regex_message
- "can contain only letters, digits, '_', '-', '@' and '.'. Separate directories with a '/'. "
+ "can contain only letters, digits, '_', '-', '@' and '.'. Separate directories with a '/'."
end
def directory_traversal_regex
@@ -60,7 +60,7 @@ module Gitlab
end
def directory_traversal_regex_message
- "cannot include directory traversal. "
+ "cannot include directory traversal."
end
def archive_formats_regex
@@ -96,11 +96,11 @@ module Gitlab
end
def environment_name_regex
- @environment_name_regex ||= /\A[a-zA-Z0-9_-]+\z/.freeze
+ @environment_name_regex ||= /\A[a-zA-Z0-9_\\\/\${}. -]+\z/.freeze
end
def environment_name_regex_message
- "can contain only letters, digits, '-' and '_'."
+ "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.' and spaces"
end
end
end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index f8ab2b1f09e..2690938fe82 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -27,11 +27,6 @@ module Gitlab
end
end
- def total_count
- @total_count ||= projects_count + issues_count + merge_requests_count +
- milestones_count
- end
-
def projects_count
@projects_count ||= projects.count
end
@@ -48,10 +43,6 @@ module Gitlab
@milestones_count ||= milestones.count
end
- def empty?
- total_count.zero?
- end
-
private
def projects
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index e0e74ff8359..9e01f02029c 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -20,10 +20,6 @@ module Gitlab
end
end
- def total_count
- @total_count ||= snippet_titles_count + snippet_blobs_count
- end
-
def snippet_titles_count
@snippet_titles_count ||= snippet_titles.count
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index c6826a09bd2..5d33f98e89e 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -1,19 +1,38 @@
require 'base64'
require 'json'
+require 'securerandom'
module Gitlab
class Workhorse
SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'
+ INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'
+ INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'
+
+ # Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
+ # bytes https://tools.ietf.org/html/rfc4868#section-2.6
+ SECRET_LENGTH = 32
class << self
def git_http_ok(repository, user)
{
- 'GL_ID' => Gitlab::GlId.gl_id(user),
- 'RepoPath' => repository.path_to_repo,
+ GL_ID: Gitlab::GlId.gl_id(user),
+ RepoPath: repository.path_to_repo,
+ }
+ end
+
+ def lfs_upload_ok(oid, size)
+ {
+ StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload",
+ LfsOid: oid,
+ LfsSize: size,
}
end
+ def artifact_upload_ok
+ { TempPath: ArtifactUploader.artifacts_upload_path }
+ end
+
def send_git_blob(repository, blob)
params = {
'RepoPath' => repository.path_to_repo,
@@ -41,7 +60,7 @@ module Gitlab
def send_git_diff(repository, diff_refs)
params = {
'RepoPath' => repository.path_to_repo,
- 'ShaFrom' => diff_refs.start_sha,
+ 'ShaFrom' => diff_refs.base_sha,
'ShaTo' => diff_refs.head_sha
}
@@ -54,7 +73,7 @@ module Gitlab
def send_git_patch(repository, diff_refs)
params = {
'RepoPath' => repository.path_to_repo,
- 'ShaFrom' => diff_refs.start_sha,
+ 'ShaFrom' => diff_refs.base_sha,
'ShaTo' => diff_refs.head_sha
}
@@ -81,6 +100,35 @@ module Gitlab
path.readable? ? path.read.chomp : 'unknown'
end
+ def secret
+ @secret ||= begin
+ bytes = Base64.strict_decode64(File.read(secret_path).chomp)
+ raise "#{secret_path} does not contain #{SECRET_LENGTH} bytes" if bytes.length != SECRET_LENGTH
+ bytes
+ end
+ end
+
+ def write_secret
+ bytes = SecureRandom.random_bytes(SECRET_LENGTH)
+ File.open(secret_path, 'w:BINARY', 0600) do |f|
+ f.chmod(0600)
+ f.write(Base64.strict_encode64(bytes))
+ end
+ end
+
+ def verify_api_request!(request_headers)
+ JWT.decode(
+ request_headers[INTERNAL_API_REQUEST_HEADER],
+ secret,
+ true,
+ { iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' },
+ )
+ end
+
+ def secret_path
+ Rails.root.join('.gitlab_workhorse_secret')
+ end
+
protected
def encode(hash)
diff --git a/lib/tasks/flog.rake b/lib/tasks/flog.rake
deleted file mode 100644
index 3bfe999ae74..00000000000
--- a/lib/tasks/flog.rake
+++ /dev/null
@@ -1,25 +0,0 @@
-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/haml-lint.rake b/lib/tasks/haml-lint.rake
new file mode 100644
index 00000000000..609dfaa48e3
--- /dev/null
+++ b/lib/tasks/haml-lint.rake
@@ -0,0 +1,5 @@
+unless Rails.env.production?
+ require 'haml_lint/rake_task'
+
+ HamlLint::RakeTask.new
+end
diff --git a/public/deploy.html b/public/deploy.html
index 142472b6c35..49ec4ac5ce1 100644
--- a/public/deploy.html
+++ b/public/deploy.html
@@ -2,6 +2,11 @@
<html>
<head>
<meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
+ <meta name="refresh" content="60">
+ <meta name="retry-after" content="100">
+ <meta name="robots" content="noindex, nofollow, noarchive, nostore">
+ <meta name="cache-control" content="no-cache, no-store">
+ <meta name="pragma" content="no-cache">
<title>Deploy in progress</title>
<style>
body {
@@ -61,4 +66,4 @@
<p>Please contact your GitLab administrator if this problem persists.</p>
</div>
</body>
-</html>
+</html> \ No newline at end of file
diff --git a/public/robots.txt b/public/robots.txt
index 334f4c03533..7d69fad59d1 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -23,7 +23,7 @@ Disallow: /groups/*/edit
Disallow: /users
# Global snippets
-Disallow: /s
+Disallow: /s/
Disallow: /snippets/new
Disallow: /snippets/*/edit
Disallow: /snippets/*/raw
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index bc6e4d94061..fb4d8463981 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -10,6 +10,15 @@ then
exit 1
fi
+# Ensure that the CHANGELOG does not contain duplicate versions
+DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^v [0-9.]+' CHANGELOG | sed 's| (unreleased)||' | sort | uniq -d)
+if [ "${DUPLICATE_CHANGELOG_VERSIONS}" != "" ]
+then
+ echo '✖ ERROR: Duplicate versions in CHANGELOG:' >&2
+ echo "${DUPLICATE_CHANGELOG_VERSIONS}" >&2
+ exit 1
+fi
+
echo "✔ Linting passed"
exit 0
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 76b2178c79c..6e987e7d9c9 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -39,7 +39,7 @@ if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
cp config/resque.yml.example config/resque.yml
sed -i 's/localhost/redis/g' config/resque.yml
- export FLAGS=(--path vendor --retry 3)
+ export FLAGS=(--path vendor --retry 3 --quiet)
else
export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
cp config/database.yml.mysql config/database.yml
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index c34475976c6..92b97bf3d0c 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -2,9 +2,10 @@ require 'spec_helper'
describe Groups::GroupMembersController do
let(:user) { create(:user) }
- let(:group) { create(:group) }
describe '#index' do
+ let(:group) { create(:group) }
+
before do
group.add_owner(user)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 07bf8d2d1c3..1d3c9fbbe2f 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -146,21 +146,42 @@ describe Import::BitbucketController do
end
context "when a namespace with the Bitbucket user's username doesn't exist" do
- it "creates the namespace" do
- expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).and_return(double(execute: true))
+ context "when current user can create namespaces" do
+ it "creates the namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
- post :create, format: :js
+ expect { post :create, format: :js }.to change(Namespace, :count).by(1)
+ end
+
+ it "takes the new namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params).
+ and_return(double(execute: true))
- expect(Namespace.where(name: other_username).first).not_to be_nil
+ post :create, format: :js
+ end
end
- it "takes the new namespace" do
- expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params).
- and_return(double(execute: true))
+ context "when current user can't create namespaces" do
+ before do
+ user.update_attribute(:can_create_group, false)
+ end
- post :create, format: :js
+ it "doesn't create the namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ expect { post :create, format: :js }.not_to change(Namespace, :count)
+ end
+
+ it "takes the current user's namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
end
end
end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 51d59526854..4f96567192d 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -124,8 +124,8 @@ describe Import::GithubController do
context "when the GitHub user and GitLab user's usernames match" do
it "takes the current user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, user.namespace, user, access_params).
- and_return(double(execute: true))
+ to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
+ and_return(double(execute: true))
post :create, format: :js
end
@@ -136,8 +136,8 @@ describe Import::GithubController do
it "takes the current user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, user.namespace, user, access_params).
- and_return(double(execute: true))
+ to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
+ and_return(double(execute: true))
post :create, format: :js
end
@@ -158,8 +158,8 @@ describe Import::GithubController do
context "when the namespace is owned by the GitLab user" do
it "takes the existing namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, existing_namespace, user, access_params).
- and_return(double(execute: true))
+ to receive(:new).with(github_repo, github_repo.name, existing_namespace, user, access_params).
+ and_return(double(execute: true))
post :create, format: :js
end
@@ -171,9 +171,10 @@ describe Import::GithubController do
existing_namespace.save
end
- it "doesn't create a project" do
+ it "creates a project using user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
- not_to receive(:new)
+ to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
+ and_return(double(execute: true))
post :create, format: :js
end
@@ -181,21 +182,63 @@ describe Import::GithubController do
end
context "when a namespace with the GitHub user's username doesn't exist" do
- it "creates the namespace" do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).and_return(double(execute: true))
+ context "when current user can create namespaces" do
+ it "creates the namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
- post :create, format: :js
+ expect { post :create, target_namespace: github_repo.name, format: :js }.to change(Namespace, :count).by(1)
+ end
+
+ it "takes the new namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(github_repo, github_repo.name, an_instance_of(Group), user, access_params).
+ and_return(double(execute: true))
+
+ post :create, target_namespace: github_repo.name, format: :js
+ end
+ end
+
+ context "when current user can't create namespaces" do
+ before do
+ user.update_attribute(:can_create_group, false)
+ end
+
+ it "doesn't create the namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ expect { post :create, format: :js }.not_to change(Namespace, :count)
+ end
- expect(Namespace.where(name: other_username).first).not_to be_nil
+ it "takes the current user's namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
end
+ end
- it "takes the new namespace" do
+ context 'user has chosen a namespace and name for the project' do
+ let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) }
+ let(:test_name) { 'test_name' }
+
+ it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, an_instance_of(Group), user, access_params).
- and_return(double(execute: true))
+ to receive(:new).with(github_repo, test_name, test_namespace, user, access_params).
+ and_return(double(execute: true))
- post :create, format: :js
+ post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js }
+ end
+
+ it 'takes the selected name and default namespace' do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(github_repo, test_name, user.namespace, user, access_params).
+ and_return(double(execute: true))
+
+ post :create, { new_name: test_name, format: :js }
end
end
end
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index e8cf6aa7767..6f75ebb16c8 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -136,21 +136,42 @@ describe Import::GitlabController do
end
context "when a namespace with the GitLab.com user's username doesn't exist" do
- it "creates the namespace" do
- expect(Gitlab::GitlabImport::ProjectCreator).
- to receive(:new).and_return(double(execute: true))
+ context "when current user can create namespaces" do
+ it "creates the namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
- post :create, format: :js
+ expect { post :create, format: :js }.to change(Namespace, :count).by(1)
+ end
+
+ it "takes the new namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params).
+ and_return(double(execute: true))
- expect(Namespace.where(name: other_username).first).not_to be_nil
+ post :create, format: :js
+ end
end
- it "takes the new namespace" do
- expect(Gitlab::GitlabImport::ProjectCreator).
- to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params).
- and_return(double(execute: true))
+ context "when current user can't create namespaces" do
+ before do
+ user.update_attribute(:can_create_group, false)
+ end
- post :create, format: :js
+ it "doesn't create the namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ expect { post :create, format: :js }.not_to change(Namespace, :count)
+ end
+
+ it "takes the current user's namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, user.namespace, user, access_params).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
end
end
end
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
index d687dea3c3b..709006a3601 100644
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -20,10 +20,7 @@ describe Projects::Boards::ListsController do
end
it 'returns a list of board lists' do
- board = project.create_board
- create(:backlog_list, board: board)
create(:list, board: board)
- create(:done_list, board: board)
read_board_list user: user
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 16929767ddf..90419368f22 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -370,6 +370,12 @@ describe Projects::IssuesController do
expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now
end
+
+ it 'delegates the update of the todos count cache to TodoService' do
+ expect_any_instance_of(TodoService).to receive(:destroy_issue).with(issue, owner).once
+
+ delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+ end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index a219400d75f..94c9edc91fe 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -320,6 +320,12 @@ describe Projects::MergeRequestsController do
expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now
end
+
+ it 'delegates the update of the todos count cache to TodoService' do
+ expect_any_instance_of(TodoService).to receive(:destroy_merge_request).with(merge_request, owner).once
+
+ delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+ end
end
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 2fe3c263524..38e02a46626 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -8,7 +8,7 @@ describe Projects::RepositoriesController do
it 'responds with redirect in correct format' do
get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip"
- expect(response.content_type).to start_with 'text/html'
+ expect(response.header["Content-Type"]).to start_with('text/html')
expect(response).to be_redirect
end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index cccd492ef06..2e44b5128b4 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -49,4 +49,20 @@ describe Projects::ServicesController do
let!(:referrer) { nil }
end
end
+
+ describe 'PUT #update' do
+ context 'on successful update' do
+ it 'sets the flash' do
+ expect(service).to receive(:to_param).and_return('hipchat')
+
+ put :update,
+ namespace_id: project.namespace.id,
+ project_id: project.id,
+ id: service.id,
+ service: { active: false }
+
+ expect(flash[:notice]).to eq 'Successfully updated.'
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index 7b3a26d7ca7..19a152bcb05 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::TemplatesController do
end
before do
- project.team.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index ffe0641ddd7..da0fdce39db 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -63,6 +63,28 @@ describe ProjectsController do
end
end
+ context "project with broken repo" do
+ let(:empty_project) { create(:project_broken_repo, :public) }
+
+ before { sign_in(user) }
+
+ User.project_views.keys.each do |project_view|
+ context "with #{project_view} view set" do
+ before do
+ user.update_attributes(project_view: project_view)
+
+ get :show, namespace_id: empty_project.namespace.path, id: empty_project.path
+ end
+
+ it "renders the empty project view" do
+ allow(Project).to receive(:repo).and_raise(Gitlab::Git::Repository::NoRepository)
+
+ expect(response).to render_template('projects/no_repo')
+ end
+ end
+ end
+ end
+
context "rendering default project view" do
render_views
@@ -181,6 +203,25 @@ describe ProjectsController do
expect(response).to have_http_status(302)
expect(response).to redirect_to(dashboard_projects_path)
end
+
+ context "when the project is forked" do
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: fork_project,
+ target_project: project)
+ end
+
+ it "closes all related merge requests" do
+ project.merge_requests << merge_request
+ sign_in(admin)
+
+ delete :destroy, namespace_id: fork_project.namespace.path, id: fork_project.path
+
+ expect(merge_request.reload.state).to eq('closed')
+ end
+ end
end
describe "POST #toggle_star" do
diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb
index 9ced397bd4a..191e290a118 100644
--- a/spec/controllers/sent_notifications_controller_spec.rb
+++ b/spec/controllers/sent_notifications_controller_spec.rb
@@ -1,25 +1,108 @@
require 'rails_helper'
describe SentNotificationsController, type: :controller do
- let(:user) { create(:user) }
- let(:issue) { create(:issue, author: user) }
- let(:sent_notification) { create(:sent_notification, noteable: issue) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:sent_notification) { create(:sent_notification, noteable: issue, recipient: user) }
- describe 'GET #unsubscribe' do
- it 'returns a 404 when calling without existing id' do
- get(:unsubscribe, id: '0' * 32)
+ let(:issue) do
+ create(:issue, project: project, author: user) do |issue|
+ issue.subscriptions.create(user: user, subscribed: true)
+ end
+ end
+
+ describe 'GET unsubscribe' do
+ context 'when the user is not logged in' do
+ context 'when the force param is passed' do
+ before { get(:unsubscribe, id: sent_notification.reply_key, force: true) }
+
+ it 'unsubscribes the user' do
+ expect(issue.subscribed?(user)).to be_falsey
+ end
+
+ it 'sets the flash message' do
+ expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ end
+
+ it 'redirects to the login page' do
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'when the force param is not passed' do
+ before { get(:unsubscribe, id: sent_notification.reply_key) }
+
+ it 'does not unsubscribe the user' do
+ expect(issue.subscribed?(user)).to be_truthy
+ end
- expect(response.status).to be 404
+ it 'does not set the flash message' do
+ expect(controller).not_to set_flash[:notice]
+ end
+
+ it 'redirects to the login page' do
+ expect(response).to render_template :unsubscribe
+ end
+ end
end
- context 'calling with id' do
- it 'shows a flash message to the user' do
- get(:unsubscribe, id: sent_notification.reply_key)
+ context 'when the user is logged in' do
+ before { sign_in(user) }
+
+ context 'when the ID passed does not exist' do
+ before { get(:unsubscribe, id: sent_notification.reply_key.reverse) }
+
+ it 'does not unsubscribe the user' do
+ expect(issue.subscribed?(user)).to be_truthy
+ end
+
+ it 'does not set the flash message' do
+ expect(controller).not_to set_flash[:notice]
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when the force param is passed' do
+ before { get(:unsubscribe, id: sent_notification.reply_key, force: true) }
+
+ it 'unsubscribes the user' do
+ expect(issue.subscribed?(user)).to be_falsey
+ end
+
+ it 'sets the flash message' do
+ expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ end
+
+ it 'redirects to the issue page' do
+ expect(response).
+ to redirect_to(namespace_project_issue_path(project.namespace, project, issue))
+ end
+ end
+
+ context 'when the force param is not passed' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project, author: user) do |merge_request|
+ merge_request.subscriptions.create(user: user, subscribed: true)
+ end
+ end
+ let(:sent_notification) { create(:sent_notification, noteable: merge_request, recipient: user) }
+ before { get(:unsubscribe, id: sent_notification.reply_key) }
+
+ it 'unsubscribes the user' do
+ expect(merge_request.subscribed?(user)).to be_falsey
+ end
- expect(response.status).to be 302
+ it 'sets the flash message' do
+ expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ end
- expect(response).to redirect_to new_user_session_path
- expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ it 'redirects to the merge request page' do
+ expect(response).
+ to redirect_to(namespace_project_merge_request_path(project.namespace, project, merge_request))
+ end
end
end
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 4e9bfb0c69b..8f27e616c3e 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -136,6 +136,29 @@ describe SessionsController do
post(:create, { user: user_params }, { otp_user_id: user.id })
end
+ context 'remember_me field' do
+ it 'sets a remember_user_token cookie when enabled' do
+ allow(U2fRegistration).to receive(:authenticate).and_return(true)
+ allow(controller).to receive(:find_user).and_return(user)
+ expect(controller).
+ to receive(:remember_me).with(user).and_call_original
+
+ authenticate_2fa_u2f(remember_me: '1', login: user.username, device_response: "{}")
+
+ expect(response.cookies['remember_user_token']).to be_present
+ end
+
+ it 'does nothing when disabled' do
+ allow(U2fRegistration).to receive(:authenticate).and_return(true)
+ allow(controller).to receive(:find_user).and_return(user)
+ expect(controller).not_to receive(:remember_me)
+
+ authenticate_2fa_u2f(remember_me: '0', login: user.username, device_response: "{}")
+
+ expect(response.cookies['remember_user_token']).to be_nil
+ end
+ end
+
it "creates an audit log record" do
allow(U2fRegistration).to receive(:authenticate).and_return(true)
expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1)
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 2a89159c070..41d263a46a4 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe SnippetsController do
- describe 'GET #show' do
- let(:user) { create(:user) }
+ let(:user) { create(:user) }
+ describe 'GET #show' do
context 'when the personal snippet is private' do
let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
@@ -230,4 +230,33 @@ describe SnippetsController do
end
end
end
+
+ context 'award emoji on snippets' do
+ let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+ let(:another_user) { create(:user) }
+
+ before do
+ sign_in(another_user)
+ end
+
+ describe 'POST #toggle_award_emoji' do
+ it "toggles the award emoji" do
+ expect do
+ post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
+ end.to change { personal_snippet.award_emoji.count }.from(0).to(1)
+
+ expect(response.status).to eq(200)
+ end
+
+ it "removes the already awarded emoji" do
+ post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
+
+ expect do
+ post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
+ end.to change { personal_snippet.award_emoji.count }.from(1).to(0)
+
+ expect(response.status).to eq(200)
+ end
+ end
+ end
end
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index 83fccad679f..3372e5ab685 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -1,14 +1,3 @@
-# == Schema Information
-#
-# Table name: runner_projects
-#
-# id :integer not null, primary key
-# runner_id :integer not null
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-#
-
FactoryGirl.define do
factory :ci_runner_project, class: Ci::RunnerProject do
runner_id 1
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index 5b645fab32e..e3b73e29987 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -1,22 +1,3 @@
-# == Schema Information
-#
-# Table name: runners
-#
-# id :integer not null, primary key
-# token :string(255)
-# created_at :datetime
-# updated_at :datetime
-# description :string(255)
-# contacted_at :datetime
-# active :boolean default(TRUE), not null
-# is_shared :boolean default(FALSE)
-# name :string(255)
-# version :string(255)
-# revision :string(255)
-# platform :string(255)
-# architecture :string(255)
-#
-
FactoryGirl.define do
factory :ci_runner, class: Ci::Runner do
sequence :description do |n|
@@ -30,5 +11,9 @@ FactoryGirl.define do
trait :shared do
is_shared true
end
+
+ trait :inactive do
+ active false
+ end
end
end
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
index 856a8e725eb..6653f0bb5c3 100644
--- a/spec/factories/ci/variables.rb
+++ b/spec/factories/ci/variables.rb
@@ -1,17 +1,3 @@
-# == Schema Information
-#
-# Table name: ci_variables
-#
-# id :integer not null, primary key
-# project_id :integer not null
-# key :string(255)
-# value :text
-# encrypted_value :text
-# encrypted_value_salt :string(255)
-# encrypted_value_iv :string(255)
-# gl_project_id :integer
-#
-
FactoryGirl.define do
factory :ci_variable, class: Ci::Variable do
sequence(:key) { |n| "VARIABLE_#{n}" }
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 82591604fcb..6f24bf58d14 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -3,11 +3,12 @@ FactoryGirl.define do
sha '97de212e80737a608d939f648d959671fb0a0142'
ref 'master'
tag false
+ project nil
environment factory: :environment
after(:build) do |deployment, evaluator|
- deployment.project = deployment.environment.project
+ deployment.project ||= deployment.environment.project
end
end
end
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index 90788f30ac9..8820d527c61 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -1,10 +1,11 @@
FactoryGirl.define do
factory :event do
+ project
+ author factory: :user
+
factory :closed_issue_event do
- project
action { Event::CLOSED }
target factory: :closed_issue
- author factory: :user
end
end
end
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index debb86d997f..795df5dfda9 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -1,20 +1,13 @@
-# == Schema Information
-#
-# Table name: group_members
-#
-# id :integer not null, primary key
-# group_access :integer not null
-# group_id :integer not null
-# user_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-# notification_level :integer default(3), not null
-#
-
FactoryGirl.define do
factory :group_member do
access_level { GroupMember::OWNER }
group
user
+
+ trait(:guest) { access_level GroupMember::GUEST }
+ trait(:reporter) { access_level GroupMember::REPORTER }
+ trait(:developer) { access_level GroupMember::DEVELOPER }
+ trait(:master) { access_level GroupMember::MASTER }
+ trait(:owner) { access_level GroupMember::OWNER }
end
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 2c0a2dd94ca..2b4670be468 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -1,4 +1,8 @@
FactoryGirl.define do
+ sequence :issue_created_at do |n|
+ 4.hours.ago + ( 2 * n ).seconds
+ end
+
factory :issue do
title
author
diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb
index e9e85962fe4..84da71ed6dc 100644
--- a/spec/factories/milestones.rb
+++ b/spec/factories/milestones.rb
@@ -3,10 +3,15 @@ FactoryGirl.define do
title
project
+ trait :active do
+ state "active"
+ end
+
trait :closed do
- state :closed
+ state "closed"
end
+ factory :active_milestone, traits: [:active]
factory :closed_milestone, traits: [:closed]
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 83e38095feb..6919002dedc 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -28,6 +28,11 @@ FactoryGirl.define do
diff_refs: noteable.diff_refs
)
end
+
+ trait :resolved do
+ resolved_at { Time.now }
+ resolved_by { create(:user) }
+ end
end
factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index cf3659ba275..1ddb305a8af 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -4,24 +4,9 @@ FactoryGirl.define do
project
master
- trait :guest do
- access_level ProjectMember::GUEST
- end
-
- trait :reporter do
- access_level ProjectMember::REPORTER
- end
-
- trait :developer do
- access_level ProjectMember::DEVELOPER
- end
-
- trait :master do
- access_level ProjectMember::MASTER
- end
-
- trait :owner do
- access_level ProjectMember::OWNER
- end
+ trait(:guest) { access_level ProjectMember::GUEST }
+ trait(:reporter) { access_level ProjectMember::REPORTER }
+ trait(:developer) { access_level ProjectMember::DEVELOPER }
+ trait(:master) { access_level ProjectMember::MASTER }
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index fb84ba07d25..873d3fcb5af 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -27,6 +27,14 @@ FactoryGirl.define do
end
end
+ trait :broken_repo do
+ after(:create) do |project|
+ project.create_repository
+
+ FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'refs'))
+ end
+ end
+
# Nest Project Feature attributes
transient do
wiki_access_level ProjectFeature::ENABLED
@@ -56,6 +64,13 @@ FactoryGirl.define do
empty_repo
end
+ # Project with broken repository
+ #
+ # Project with an invalid repository state
+ factory :project_broken_repo, parent: :empty_project do
+ broken_repo
+ end
+
# Project with test repository
#
# Test repository source can be found at
@@ -106,6 +121,8 @@ FactoryGirl.define do
factory :project_with_board, parent: :empty_project do
after(:create) do |project|
project.create_board
+ project.board.lists.create(list_type: :backlog)
+ project.board.lists.create(list_type: :done)
end
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index c6c2e2095df..26ea06e002b 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -2,16 +2,13 @@ require 'rails_helper'
describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
+ include WaitForVueResource
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:project_with_board, :public) }
let(:user) { create(:user) }
let!(:user2) { create(:user) }
before do
- project.create_board
- project.board.lists.create(list_type: :backlog)
- project.board.lists.create(list_type: :done)
-
project.team << [user, :master]
project.team << [user2, :master]
@@ -61,6 +58,7 @@ describe 'Issue Boards', feature: true, js: true do
let(:bug) { create(:label, project: project, name: 'Bug') }
let!(:backlog) { create(:label, project: project, name: 'Backlog') }
let!(:done) { create(:label, project: project, name: 'Done') }
+ let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') }
let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
@@ -74,7 +72,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
let!(:issue8) { create(:closed_issue, project: project) }
- let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug]) }
+ let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) }
before do
visit namespace_project_board_path(project.namespace, project)
@@ -93,15 +91,8 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'shows issues in lists' do
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('2')
- expect(page).to have_selector('.card', count: 2)
- end
-
- page.within(find('.board:nth-child(3)')) do
- expect(page.find('.board-header')).to have_content('2')
- expect(page).to have_selector('.card', count: 2)
- end
+ wait_for_board_cards(2, 2)
+ wait_for_board_cards(3, 2)
end
it 'shows confidential issues with icon' do
@@ -187,13 +178,13 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_content('Showing 20 of 56 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
expect(page).to have_selector('.card', count: 40)
expect(page).to have_content('Showing 40 of 56 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
expect(page).to have_selector('.card', count: 56)
expect(page).to have_content('Showing all issues')
@@ -202,37 +193,33 @@ describe 'Issue Boards', feature: true, js: true do
context 'backlog' do
it 'shows issues in backlog with no labels' do
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('6')
- expect(page).to have_selector('.card', count: 6)
- end
+ wait_for_board_cards(1, 6)
end
it 'moves issue from backlog into list' do
drag_to(list_to_index: 1)
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('5')
- expect(page).to have_selector('.card', count: 5)
- end
-
wait_for_vue_resource
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('3')
- expect(page).to have_selector('.card', count: 3)
- end
+ wait_for_board_cards(1, 5)
+ wait_for_board_cards(2, 3)
end
end
context 'done' do
it 'shows list of done issues' do
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+ wait_for_board_cards(4, 1)
+ wait_for_ajax
end
it 'moves issue to done' do
drag_to(list_from_index: 0, list_to_index: 3)
+ wait_for_board_cards(1, 5)
+ wait_for_board_cards(2, 2)
+ wait_for_board_cards(3, 2)
+ wait_for_board_cards(4, 2)
+
+ expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
expect(find('.board:nth-child(4)')).to have_content(issue9.title)
expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
@@ -241,8 +228,12 @@ describe 'Issue Boards', feature: true, js: true do
it 'removes all of the same issue to done' do
drag_to(list_from_index: 1, list_to_index: 3)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
+ wait_for_board_cards(1, 6)
+ wait_for_board_cards(2, 1)
+ wait_for_board_cards(3, 1)
+ wait_for_board_cards(4, 2)
+
+ expect(find('.board:nth-child(2)')).not_to have_content(issue6.title)
expect(find('.board:nth-child(4)')).to have_content(issue6.title)
expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
end
@@ -252,6 +243,11 @@ describe 'Issue Boards', feature: true, js: true do
it 'changes position of list' do
drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header')
+ wait_for_board_cards(1, 6)
+ wait_for_board_cards(2, 2)
+ wait_for_board_cards(3, 2)
+ wait_for_board_cards(4, 1)
+
expect(find('.board:nth-child(2)')).to have_content(development.title)
expect(find('.board:nth-child(2)')).to have_content(planning.title)
end
@@ -259,8 +255,11 @@ describe 'Issue Boards', feature: true, js: true do
it 'issue moves between lists' do
drag_to(list_from_index: 1, card_index: 1, list_to_index: 2)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 3)
+ wait_for_board_cards(1, 6)
+ wait_for_board_cards(2, 1)
+ wait_for_board_cards(3, 3)
+ wait_for_board_cards(4, 1)
+
expect(find('.board:nth-child(3)')).to have_content(issue6.title)
expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)
end
@@ -268,8 +267,11 @@ describe 'Issue Boards', feature: true, js: true do
it 'issue moves between lists' do
drag_to(list_from_index: 2, list_to_index: 1)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
+ wait_for_board_cards(1, 6)
+ wait_for_board_cards(2, 3)
+ wait_for_board_cards(3, 1)
+ wait_for_board_cards(4, 1)
+
expect(find('.board:nth-child(2)')).to have_content(issue7.title)
expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)
end
@@ -277,8 +279,12 @@ describe 'Issue Boards', feature: true, js: true do
it 'issue moves from done' do
drag_to(list_from_index: 3, list_to_index: 1)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3)
expect(find('.board:nth-child(2)')).to have_content(issue8.title)
+
+ wait_for_board_cards(1, 6)
+ wait_for_board_cards(2, 3)
+ wait_for_board_cards(3, 2)
+ wait_for_board_cards(4, 0)
end
context 'issue card' do
@@ -341,10 +347,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'moves issues from backlog into new list' do
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('6')
- expect(page).to have_selector('.card', count: 6)
- end
+ wait_for_board_cards(1, 6)
click_button 'Create new list'
wait_for_ajax
@@ -355,10 +358,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('5')
- expect(page).to have_selector('.card', count: 5)
- end
+ wait_for_board_cards(1, 5)
end
end
end
@@ -372,22 +372,14 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.dropdown-menu-author' do
click_link(user2.name)
end
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
expect(find('.js-author-search')).to have_content(user2.name)
end
wait_for_vue_resource
-
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
- end
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
+ wait_for_board_cards(1, 1)
+ wait_for_empty_boards((2..4))
end
it 'filters by assignee' do
@@ -398,22 +390,15 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.dropdown-menu-assignee' do
click_link(user.name)
end
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
expect(find('.js-assignee-search')).to have_content(user.name)
end
wait_for_vue_resource
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
- end
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
+ wait_for_board_cards(1, 1)
+ wait_for_empty_boards((2..4))
end
it 'filters by milestone' do
@@ -424,36 +409,50 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.milestone-filter' do
click_link(milestone.title)
end
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
expect(find('.js-milestone-select')).to have_content(milestone.title)
end
wait_for_vue_resource
+ wait_for_board_cards(1, 0)
+ wait_for_board_cards(2, 1)
+ wait_for_board_cards(3, 0)
+ wait_for_board_cards(4, 0)
+ end
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
+ it 'filters by label' do
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
+ page.within '.dropdown-menu-labels' do
+ click_link(testing.title)
+ wait_for_vue_resource
+ find('.dropdown-menu-close').click
+ end
end
+
+ wait_for_vue_resource
+ wait_for_board_cards(1, 1)
+ wait_for_empty_boards((2..4))
end
- it 'filters by label' do
+ it 'filters by label with space after reload' do
page.within '.issues-filters' do
click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do
- click_link(testing.title)
+ click_link(accepting.title)
wait_for_vue_resource(spinner: false)
find('.dropdown-menu-close').click
end
end
+ # Test after reload
+ page.evaluate_script 'window.location.reload()'
+
wait_for_vue_resource
page.within(find('.board', match: :first)) do
@@ -467,6 +466,29 @@ describe 'Issue Boards', feature: true, js: true do
end
end
+ it 'removes filtered labels' do
+ wait_for_vue_resource
+
+ page.within '.labels-filter' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-labels' do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ end
+
+ expect(page).to have_css('input[name="label_name[]"]', visible: false)
+
+ page.within '.dropdown-menu-labels' do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ end
+
+ expect(page).not_to have_css('input[name="label_name[]"]', visible: false)
+ end
+ end
+
it 'infinite scrolls list with label filter' do
50.times do
create(:labeled_issue, project: project, labels: [testing])
@@ -478,7 +500,7 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.dropdown-menu-labels' do
click_link(testing.title)
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
find('.dropdown-menu-close').click
end
end
@@ -509,24 +531,17 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.dropdown-menu-labels')) do
click_link(testing.title)
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
click_link(bug.title)
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
find('.dropdown-menu-close').click
end
end
wait_for_vue_resource
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
- end
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
+ wait_for_board_cards(1, 1)
+ wait_for_empty_boards((2..4))
end
it 'filters by no label' do
@@ -536,22 +551,17 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.dropdown-menu-labels' do
click_link("No Label")
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
find('.dropdown-menu-close').click
end
end
wait_for_vue_resource
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('5')
- expect(page).to have_selector('.card', count: 5)
- end
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
+ wait_for_board_cards(1, 5)
+ wait_for_board_cards(2, 0)
+ wait_for_board_cards(3, 0)
+ wait_for_board_cards(4, 1)
end
it 'filters by clicking label button on issue' do
@@ -559,20 +569,13 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.card', count: 6)
expect(find('.card', match: :first)).to have_content(bug.title)
click_button(bug.title)
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
end
wait_for_vue_resource
- page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
- end
-
- page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
- end
+ wait_for_board_cards(1, 1)
+ wait_for_empty_boards((2..4))
page.within('.labels-filter') do
expect(find('.dropdown-toggle-text')).to have_content(bug.title)
@@ -584,7 +587,7 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.card', match: :first)) do
click_button(bug.title)
end
- wait_for_vue_resource(spinner: false)
+ wait_for_vue_resource
expect(page).to have_selector('.card', count: 1)
end
@@ -648,13 +651,16 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
end
- def wait_for_vue_resource(spinner: true)
- Timeout.timeout(Capybara.default_max_wait_time) do
- loop until page.evaluate_script('Vue.activeResources').zero?
+ def wait_for_board_cards(board_number, expected_cards)
+ page.within(find(".board:nth-child(#{board_number})")) do
+ expect(page.find('.board-header')).to have_content(expected_cards.to_s)
+ expect(page).to have_selector('.card', count: expected_cards)
end
+ end
- if spinner
- expect(find('.boards-list')).not_to have_selector('.fa-spinner')
+ def wait_for_empty_boards(board_numbers)
+ board_numbers.each do |board|
+ wait_for_board_cards(board, 0)
end
end
end
diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb
new file mode 100644
index 00000000000..7ef68e9eb8d
--- /dev/null
+++ b/spec/features/boards/keyboard_shortcut_spec.rb
@@ -0,0 +1,24 @@
+require 'rails_helper'
+
+describe 'Issue Boards shortcut', feature: true, js: true do
+ include WaitForVueResource
+
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.create_board
+ project.board.lists.create(list_type: :backlog)
+ project.board.lists.create(list_type: :done)
+
+ login_as :admin
+
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'takes user to issue board index' do
+ find('body').native.send_keys('gl')
+ expect(page).to have_selector('.boards-list')
+
+ wait_for_vue_resource
+ end
+end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
new file mode 100644
index 00000000000..7fa0c95cae2
--- /dev/null
+++ b/spec/features/calendar_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+feature 'Contributions Calendar', js: true, feature: true do
+ include WaitForAjax
+
+ let(:contributed_project) { create(:project, :public) }
+
+ # Ex/ Sunday Jan 1, 2016
+ date_format = '%A %b %-d, %Y'
+
+ issue_title = 'Bug in old browser'
+ issue_params = { title: issue_title }
+
+ def get_cell_color_selector(contributions)
+ contribution_cell = '.user-contrib-cell'
+ activity_colors = Array['#ededed', '#acd5f2', '#7fa8c9', '#527ba0', '#254e77']
+ activity_colors_index = 0
+
+ if contributions > 0 && contributions < 10
+ activity_colors_index = 1
+ elsif contributions >= 10 && contributions < 20
+ activity_colors_index = 2
+ elsif contributions >= 20 && contributions < 30
+ activity_colors_index = 3
+ elsif contributions >= 30
+ activity_colors_index = 4
+ end
+
+ "#{contribution_cell}[fill='#{activity_colors[activity_colors_index]}']"
+ end
+
+ def get_cell_date_selector(contributions, date)
+ contribution_text = 'No contributions'
+
+ if contributions === 1
+ contribution_text = '1 contribution'
+ elsif contributions > 1
+ contribution_text = "#{contributions} contributions"
+ end
+
+ "#{get_cell_color_selector(contributions)}[data-original-title='#{contribution_text}<br />#{date}']"
+ end
+
+ def push_code_contribution
+ push_params = {
+ project: contributed_project,
+ action: Event::PUSHED,
+ author_id: @user.id,
+ data: { commit_count: 3 }
+ }
+
+ Event.create(push_params)
+ end
+
+ before do
+ login_as :user
+ visit @user.username
+ wait_for_ajax
+ end
+
+ it 'displays calendar', js: true do
+ expect(page).to have_css('.js-contrib-calendar')
+ end
+
+ describe '1 calendar activity' do
+ before do
+ Issues::CreateService.new(contributed_project, @user, issue_params).execute
+ visit @user.username
+ wait_for_ajax
+ end
+
+ it 'displays calendar activity log', js: true do
+ expect(find('.content_list .event-note')).to have_content issue_title
+ end
+
+ it 'displays calendar activity square color for 1 contribution', js: true do
+ expect(page).to have_selector(get_cell_color_selector(1), count: 1)
+ end
+
+ it 'displays calendar activity square on the correct date', js: true do
+ today = Date.today.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
+ end
+ end
+
+ describe '10 calendar activities' do
+ before do
+ (0..9).each do |i|
+ push_code_contribution()
+ end
+
+ visit @user.username
+ wait_for_ajax
+ end
+
+ it 'displays calendar activity square color for 10 contributions', js: true do
+ expect(page).to have_selector(get_cell_color_selector(10), count: 1)
+ end
+
+ it 'displays calendar activity square on the correct date', js: true do
+ today = Date.today.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(10, today), count: 1)
+ end
+ end
+
+ describe 'calendar activity on two days' do
+ before do
+ push_code_contribution()
+
+ Timecop.freeze(Date.yesterday)
+ Issues::CreateService.new(contributed_project, @user, issue_params).execute
+ Timecop.return
+
+ visit @user.username
+ wait_for_ajax
+ end
+
+ it 'displays calendar activity squares for both days', js: true do
+ expect(page).to have_selector(get_cell_color_selector(1), count: 2)
+ end
+
+ it 'displays calendar activity square for yesterday', js: true do
+ yesterday = Date.yesterday.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
+ end
+
+ it 'displays calendar activity square for today', js: true do
+ today = Date.today.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
+ end
+ end
+end
diff --git a/spec/features/compare_spec.rb b/spec/features/compare_spec.rb
index ca7f73e24cc..33dfd0d5b62 100644
--- a/spec/features/compare_spec.rb
+++ b/spec/features/compare_spec.rb
@@ -12,15 +12,16 @@ describe "Compare", js: true do
describe "branches" do
it "pre-populates fields" do
- expect(page.find_field("from").value).to eq("master")
+ expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("master")
+ expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master")
end
it "compares branches" do
- fill_in "from", with: "fea"
- find("#from").click
+ select_using_dropdown "from", "feature"
+ expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("feature")
- click_link "feature"
- expect(page.find_field("from").value).to eq("feature")
+ select_using_dropdown "to", "binary-encoding"
+ expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("binary-encoding")
click_button "Compare"
expect(page).to have_content "Commits"
@@ -29,14 +30,21 @@ describe "Compare", js: true do
describe "tags" do
it "compares tags" do
- fill_in "from", with: "v1.0"
- find("#from").click
+ select_using_dropdown "from", "v1.0.0"
+ expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("v1.0.0")
- click_link "v1.0.0"
- expect(page.find_field("from").value).to eq("v1.0.0")
+ select_using_dropdown "to", "v1.1.0"
+ expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("v1.1.0")
click_button "Compare"
expect(page).to have_content "Commits"
end
end
+
+ def select_using_dropdown(dropdown_type, selection)
+ dropdown = find(".js-compare-#{dropdown_type}-dropdown")
+ dropdown.find(".compare-dropdown-toggle").click
+ dropdown.fill_in("Filter by branch/tag", with: selection)
+ click_link selection
+ end
end
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
new file mode 100644
index 00000000000..62937688c22
--- /dev/null
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'Dashboard snippets', feature: true do
+ context 'when the project has snippets' do
+ let(:project) { create(:empty_project, :public) }
+ let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+ login_as(project.owner)
+ visit dashboard_snippets_path
+ end
+
+ it_behaves_like 'paginated snippets'
+ end
+end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index 3fb1cb37544..9b54b5301e5 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -21,6 +21,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
click_link 'No Milestone'
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1)
end
@@ -29,6 +30,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
click_link 'Any Milestone'
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
expect(page).to have_selector('.issue', count: 2)
end
@@ -39,6 +41,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
click_link milestone.title
end
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1)
end
end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index fcd41b38413..4309a726917 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -150,7 +150,7 @@ feature 'Environments', feature: true do
context 'for invalid name' do
before do
- fill_in('Name', with: 'name with spaces')
+ fill_in('Name', with: 'name,with,commas')
click_on 'Save'
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 688f68d3cff..6c938bdead8 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -68,7 +68,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding a diff for a renamed file' do
before do
- large_diff_renamed.find('.nothing-here-block').click
+ large_diff_renamed.find('.click-to-expand').click
wait_for_ajax
end
@@ -87,7 +87,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding a large diff' do
before do
- click_link('large_diff.md')
+ # Wait for diffs
+ find('.file-title', match: :first)
+ # Click `large_diff.md` title
+ all('.file-title')[1].click
wait_for_ajax
end
@@ -128,7 +131,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding the diff' do
before do
- click_link('large_diff.md')
+ # Wait for diffs
+ find('.file-title', match: :first)
+ # Click `large_diff.md` title
+ all('.file-title')[1].click
wait_for_ajax
end
@@ -146,7 +152,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end
context 'collapsing an expanded diff' do
- before { click_link('small_diff.md') }
+ before do
+ # Wait for diffs
+ find('.file-title', match: :first)
+ # Click `small_diff.md` title
+ all('.file-title')[3].click
+ end
it 'hides the diff content' do
expect(small_diff).not_to have_selector('.code')
@@ -154,7 +165,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end
context 're-expanding the same diff' do
- before { click_link('small_diff.md') }
+ before do
+ # Wait for diffs
+ find('.file-title', match: :first)
+ # Click `small_diff.md` title
+ all('.file-title')[3].click
+ end
it 'shows the diff content' do
expect(small_diff).to have_selector('.code')
@@ -211,6 +227,13 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding all diffs' do
before do
click_link('Expand all')
+
+ # Wait for elements to appear to ensure full page reload
+ expect(page).to have_content('This diff was suppressed by a .gitattributes entry')
+ expect(page).to have_content('This diff could not be displayed because it is too large.')
+ expect(page).to have_content('too_large_image.jpg')
+ find('.note-textarea')
+
wait_for_ajax
execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });')
end
@@ -224,7 +247,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end
context 'collapsing an expanded diff' do
- before { click_link('small_diff.md') }
+ before do
+ # Wait for diffs
+ find('.file-title', match: :first)
+ # Click `small_diff.md` title
+ all('.file-title')[3].click
+ end
it 'hides the diff content' do
expect(small_diff).not_to have_selector('.code')
@@ -232,7 +260,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end
context 're-expanding the same diff' do
- before { click_link('small_diff.md') }
+ before do
+ # Wait for diffs
+ find('.file-title', match: :first)
+ # Click `small_diff.md` title
+ all('.file-title')[3].click
+ end
it 'shows the diff content' do
expect(small_diff).to have_selector('.code')
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
index 908b18e5339..0253629f753 100644
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ b/spec/features/issues/filter_by_labels_spec.rb
@@ -1,10 +1,10 @@
require 'rails_helper'
-feature 'Issue filtering by Labels', feature: true do
+feature 'Issue filtering by Labels', feature: true, js: true do
include WaitForAjax
let(:project) { create(:project, :public) }
- let!(:user) { create(:user)}
+ let!(:user) { create(:user) }
let!(:label) { create(:label, project: project) }
before do
@@ -28,156 +28,81 @@ feature 'Issue filtering by Labels', feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
- context 'filter by label bug', js: true do
+ context 'filter by label bug' do
before do
- page.find('.js-label-select').click
- wait_for_ajax
- execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
+ select_labels('bug')
end
- it 'shows issue "Bugfix1" and "Bugfix2" in issues list' do
+ it 'apply the filter' do
expect(page).to have_content "Bugfix1"
expect(page).to have_content "Bugfix2"
- end
-
- it 'does not show "Feature1" in issues list' do
expect(page).not_to have_content "Feature1"
- end
-
- it 'shows label "bug" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "bug"
- end
-
- it 'does not show label "feature" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "feature"
expect(find('.filtered-labels')).not_to have_content "enhancement"
- end
- it 'removes label "bug"' do
find('.js-label-filter-remove').click
wait_for_ajax
expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
end
end
- context 'filter by label feature', js: true do
+ context 'filter by label feature' do
before do
- page.find('.js-label-select').click
- wait_for_ajax
- execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
+ select_labels('feature')
end
- it 'shows issue "Feature1" in issues list' do
+ it 'applies the filter' do
expect(page).to have_content "Feature1"
- end
-
- it 'does not show "Bugfix1" and "Bugfix2" in issues list' do
expect(page).not_to have_content "Bugfix2"
expect(page).not_to have_content "Bugfix1"
- end
-
- it 'shows label "feature" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "feature"
- end
-
- it 'does not show label "bug" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "enhancement"
end
end
- context 'filter by label enhancement', js: true do
+ context 'filter by label enhancement' do
before do
- page.find('.js-label-select').click
- wait_for_ajax
- execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
+ select_labels('enhancement')
end
- it 'shows issue "Bugfix2" in issues list' do
+ it 'applies the filter' do
expect(page).to have_content "Bugfix2"
- end
-
- it 'does not show "Feature1" and "Bugfix1" in issues list' do
expect(page).not_to have_content "Feature1"
expect(page).not_to have_content "Bugfix1"
- end
-
- it 'shows label "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "enhancement"
- end
-
- it 'does not show label "feature" and "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "feature"
end
end
- context 'filter by label enhancement or feature', js: true do
+ context 'filter by label enhancement and bug in issues list' do
before do
- page.find('.js-label-select').click
- wait_for_ajax
- execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
- execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
+ select_labels('bug', 'enhancement')
end
- it 'does not show "Bugfix1" or "Feature1" in issues list' do
- expect(page).not_to have_content "Bugfix1"
+ it 'applies the filters' do
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1"
- end
-
- it 'shows label "enhancement" and "feature" in filtered-labels' do
+ expect(find('.filtered-labels')).to have_content "bug"
expect(find('.filtered-labels')).to have_content "enhancement"
- expect(find('.filtered-labels')).to have_content "feature"
- end
-
- it 'does not show label "bug" in filtered-labels' do
- expect(find('.filtered-labels')).not_to have_content "bug"
- end
+ expect(find('.filtered-labels')).not_to have_content "feature"
- it 'removes label "enhancement"' do
find('.js-label-filter-remove', match: :first).click
wait_for_ajax
- expect(find('.filtered-labels')).to have_no_content "enhancement"
- end
- end
-
- context 'filter by label enhancement and bug in issues list', js: true do
- before do
- page.find('.js-label-select').click
- wait_for_ajax
- execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
- execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
- end
- it 'shows issue "Bugfix2" in issues list' do
expect(page).to have_content "Bugfix2"
- end
-
- it 'does not show "Feature1"' do
expect(page).not_to have_content "Feature1"
- end
-
- it 'shows label "bug" and "enhancement" in filtered-labels' do
- expect(find('.filtered-labels')).to have_content "bug"
+ expect(page).not_to have_content "Bugfix1"
+ expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).to have_content "enhancement"
- end
-
- it 'does not show label "feature" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "feature"
end
end
- context 'remove filtered labels', js: true do
+ context 'remove filtered labels' do
before do
page.within '.labels-filter' do
click_button 'Label'
@@ -200,7 +125,7 @@ feature 'Issue filtering by Labels', feature: true do
end
end
- context 'dropdown filtering', js: true do
+ context 'dropdown filtering' do
it 'filters by label name' do
page.within '.labels-filter' do
click_button 'Label'
@@ -214,4 +139,14 @@ feature 'Issue filtering by Labels', feature: true do
end
end
end
+
+ def select_labels(*labels)
+ page.find('.js-label-select').click
+ wait_for_ajax
+ labels.each do |label|
+ execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()")
+ end
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ wait_for_ajax
+ end
end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 24e5491f087..32aaa0f1037 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -7,15 +7,15 @@ describe 'Filter issues', feature: true do
let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
- let!(:issue1) { create(:issue, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
before do
project.team << [user, :master]
login_as(user)
+ create(:issue, project: project)
end
- describe 'Filter issues for assignee from issues#index' do
+ describe 'for assignee from issues#index' do
before do
visit namespace_project_issues_path(project.namespace, project)
@@ -45,7 +45,7 @@ describe 'Filter issues', feature: true do
end
end
- describe 'Filter issues for milestone from issues#index' do
+ describe 'for milestone from issues#index' do
before do
visit namespace_project_issues_path(project.namespace, project)
@@ -75,7 +75,7 @@ describe 'Filter issues', feature: true do
end
end
- describe 'Filter issues for label from issues#index', js: true do
+ describe 'for label from issues#index', js: true do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-label-select').click
@@ -101,7 +101,7 @@ describe 'Filter issues', feature: true do
expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels')
end
- it 'filters by no label' do
+ it 'filters by a label' do
find('.dropdown-menu-labels a', text: label.title).click
page.within '.labels-filter' do
expect(page).to have_content label.title
@@ -109,16 +109,45 @@ describe 'Filter issues', feature: true do
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
- it 'filters by wont fix labels' do
+ it "filters by `won't fix` and another label" do
+ find('.dropdown-menu-labels a', text: label.title).click
page.within '.labels-filter' do
expect(page).to have_content wontfix.title
click_link wontfix.title
end
+
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title)
end
+
+ it "filters by `won't fix` label followed by another label after page load" do
+ find('.dropdown-menu-labels a', text: wontfix.title).click
+ # Close label dropdown to load
+ find('body').click
+ expect(find('.filtered-labels')).to have_content(wontfix.title)
+
+ find('.js-label-select').click
+ wait_for_ajax
+ find('.dropdown-menu-labels a', text: label.title).click
+ # Close label dropdown to load
+ find('body').click
+ expect(find('.filtered-labels')).to have_content(label.title)
+
+ find('.js-label-select').click
+ wait_for_ajax
+ expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active')
+ expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active')
+ end
+
+ it "selects and unselects `won't fix`" do
+ find('.dropdown-menu-labels a', text: wontfix.title).click
+ find('.dropdown-menu-labels a', text: wontfix.title).click
+ # Close label dropdown to load
+ find('body').click
+ expect(page).not_to have_css('.filtered-labels')
+ end
end
- describe 'Filter issues for assignee and label from issues#index' do
+ describe 'for assignee and label from issues#index' do
before do
visit namespace_project_issues_path(project.namespace, project)
@@ -178,7 +207,7 @@ describe 'Filter issues', feature: true do
context 'only text', js: true do
it 'filters issues by searched text' do
- fill_in 'issue_search', with: 'Bug'
+ fill_in 'issuable_search', with: 'Bug'
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
@@ -186,7 +215,7 @@ describe 'Filter issues', feature: true do
end
it 'does not show any issues' do
- fill_in 'issue_search', with: 'testing'
+ fill_in 'issuable_search', with: 'testing'
page.within '.issues-list' do
expect(page).not_to have_selector('.issue')
@@ -196,8 +225,9 @@ describe 'Filter issues', feature: true do
context 'text and dropdown options', js: true do
it 'filters by text and label' do
- fill_in 'issue_search', with: 'Bug'
+ fill_in 'issuable_search', with: 'Bug'
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
end
@@ -208,14 +238,16 @@ describe 'Filter issues', feature: true do
end
find('.dropdown-menu-close-icon').click
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1)
end
end
it 'filters by text and milestone' do
- fill_in 'issue_search', with: 'Bug'
+ fill_in 'issuable_search', with: 'Bug'
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
end
@@ -225,14 +257,16 @@ describe 'Filter issues', feature: true do
click_link '8'
end
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1)
end
end
it 'filters by text and assignee' do
- fill_in 'issue_search', with: 'Bug'
+ fill_in 'issuable_search', with: 'Bug'
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
end
@@ -242,14 +276,16 @@ describe 'Filter issues', feature: true do
click_link user.name
end
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1)
end
end
it 'filters by text and author' do
- fill_in 'issue_search', with: 'Bug'
+ fill_in 'issuable_search', with: 'Bug'
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
end
@@ -259,6 +295,7 @@ describe 'Filter issues', feature: true do
click_link user.name
end
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1)
end
@@ -287,6 +324,7 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-close-icon').click
wait_for_ajax
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
end
diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb
new file mode 100644
index 00000000000..f4d0f13c3d5
--- /dev/null
+++ b/spec/features/issues/reset_filters_spec.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+feature 'Issues filter reset button', feature: true, js: true do
+ include WaitForAjax
+ include IssueHelpers
+
+ let!(:project) { create(:project, :public) }
+ let!(:user) { create(:user)}
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:bug) { create(:label, project: project, name: 'bug')}
+ let!(:issue1) { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')}
+ let!(:issue2) { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')}
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when a milestone filter has been applied' do
+ it 'resets the milestone filter' do
+ visit_issues(project, milestone_title: milestone.title)
+ expect(page).to have_css('.issue', count: 1)
+
+ reset_filters
+ expect(page).to have_css('.issue', count: 2)
+ end
+ end
+
+ context 'when a label filter has been applied' do
+ it 'resets the label filter' do
+ visit_issues(project, label_name: bug.name)
+ expect(page).to have_css('.issue', count: 1)
+
+ reset_filters
+ expect(page).to have_css('.issue', count: 2)
+ end
+ end
+
+ context 'when a text search has been conducted' do
+ it 'resets the text search filter' do
+ visit_issues(project, search: 'Bug')
+ expect(page).to have_css('.issue', count: 1)
+
+ reset_filters
+ expect(page).to have_css('.issue', count: 2)
+ end
+ end
+
+ context 'when author filter has been applied' do
+ it 'resets the author filter' do
+ visit_issues(project, author_id: user.id)
+ expect(page).to have_css('.issue', count: 1)
+
+ reset_filters
+ expect(page).to have_css('.issue', count: 2)
+ end
+ end
+
+ context 'when assignee filter has been applied' do
+ it 'resets the assignee filter' do
+ visit_issues(project, assignee_id: user.id)
+ expect(page).to have_css('.issue', count: 1)
+
+ reset_filters
+ expect(page).to have_css('.issue', count: 2)
+ end
+ end
+
+ context 'when all filters have been applied' do
+ it 'resets all filters' do
+ visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
+ expect(page).to have_css('.issue', count: 0)
+
+ reset_filters
+ expect(page).to have_css('.issue', count: 2)
+ end
+ end
+
+ def reset_filters
+ find('.reset-filters').click
+ end
+end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 2883e392694..bf2b93c92fb 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
feature 'Issues > User uses slash commands', feature: true, js: true do
+ include SlashCommandsHelpers
include WaitForAjax
it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do
@@ -17,41 +18,85 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
+ after do
+ wait_for_ajax
+ end
+
describe 'adding a due date from note' do
let(:issue) { create(:issue, project: project) }
- it 'does not create a note, and sets the due date accordingly' do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/due 2016-08-28"
- click_button 'Comment'
+ context 'when the current user can update the due date' do
+ it 'does not create a note, and sets the due date accordingly' do
+ write_note("/due 2016-08-28")
+
+ expect(page).not_to have_content '/due 2016-08-28'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ issue.reload
+
+ expect(issue.due_date).to eq Date.new(2016, 8, 28)
end
+ end
+
+ context 'when the current user cannot update the due date' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ logout
+ login_with(guest)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'does not create a note, and sets the due date accordingly' do
+ write_note("/due 2016-08-28")
- expect(page).not_to have_content '/due 2016-08-28'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content '/due 2016-08-28'
+ expect(page).not_to have_content 'Your commands have been executed!'
- issue.reload
+ issue.reload
- expect(issue.due_date).to eq Date.new(2016, 8, 28)
+ expect(issue.due_date).to be_nil
+ end
end
end
describe 'removing a due date from note' do
let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
- it 'does not create a note, and removes the due date accordingly' do
- expect(issue.due_date).to eq Date.new(2016, 8, 28)
+ context 'when the current user can update the due date' do
+ it 'does not create a note, and removes the due date accordingly' do
+ expect(issue.due_date).to eq Date.new(2016, 8, 28)
+
+ write_note("/remove_due_date")
+
+ expect(page).not_to have_content '/remove_due_date'
+ expect(page).to have_content 'Your commands have been executed!'
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/remove_due_date"
- click_button 'Comment'
+ issue.reload
+
+ expect(issue.due_date).to be_nil
end
+ end
- expect(page).not_to have_content '/remove_due_date'
- expect(page).to have_content 'Your commands have been executed!'
+ context 'when the current user cannot update the due date' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ logout
+ login_with(guest)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'does not create a note, and sets the due date accordingly' do
+ write_note("/remove_due_date")
- issue.reload
+ expect(page).to have_content '/remove_due_date'
+ expect(page).not_to have_content 'Your commands have been executed!'
- expect(issue.due_date).to be_nil
+ issue.reload
+
+ expect(issue.due_date).to eq Date.new(2016, 8, 28)
+ end
end
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 36ef28ae992..b504329656f 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -143,7 +143,7 @@ describe 'Issues', feature: true do
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
expect(page).to have_content 'foobar'
- expect(page.all('.issue-no-comments').first.text).to eq "0"
+ expect(page.all('.no-comments').first.text).to eq "0"
end
end
@@ -368,6 +368,24 @@ describe 'Issues', feature: true do
end
end
+ describe 'update labels from issue#show', js: true do
+ let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
+ let!(:label) { create(:label, project: project) }
+
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'will not send ajax request when no data is changed' do
+ page.within '.labels' do
+ click_link 'Edit'
+ first('.dropdown-menu-close').click
+
+ expect(page).not_to have_selector('.block-loading')
+ end
+ end
+ end
+
describe 'update assignee from issue#show' do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index bb0bb590a46..d917d5950ec 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -17,6 +17,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project)
filter_by_milestone(Milestone::None.title)
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
@@ -39,6 +40,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project)
filter_by_milestone(Milestone::Upcoming.title)
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
@@ -61,6 +63,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project)
filter_by_milestone(milestone.title)
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb
index 577c910f11b..23cee891bac 100644
--- a/spec/features/merge_requests/merge_request_versions_spec.rb
+++ b/spec/features/merge_requests/merge_request_versions_spec.rb
@@ -1,18 +1,19 @@
require 'spec_helper'
feature 'Merge Request versions', js: true, feature: true do
+ let(:merge_request) { create(:merge_request, importing: true) }
+ let(:project) { merge_request.source_project }
+
before do
login_as :admin
- merge_request = create(:merge_request, importing: true)
merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
- project = merge_request.source_project
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'show the latest version of the diff' do
- page.within '.mr-version-switch' do
- expect(page).to have_content 'Version: latest'
+ page.within '.mr-version-dropdown' do
+ expect(page).to have_content 'latest version'
end
expect(page).to have_content '8 changed files'
@@ -20,18 +21,63 @@ feature 'Merge Request versions', js: true, feature: true do
describe 'switch between versions' do
before do
- page.within '.mr-version-switch' do
- find('.btn-link').click
- click_link '6f6d7e7e'
+ page.within '.mr-version-dropdown' do
+ find('.btn-default').click
+ click_link 'version 1'
end
end
it 'should show older version' do
- page.within '.mr-version-switch' do
- expect(page).to have_content 'Version: 6f6d7e7e'
+ page.within '.mr-version-dropdown' do
+ expect(page).to have_content 'version 1'
end
expect(page).to have_content '5 changed files'
end
+
+ it 'show the message about disabled comments' do
+ expect(page).to have_content 'Comments are disabled'
+ end
+ end
+
+ describe 'compare with older version' do
+ before do
+ page.within '.mr-version-compare-dropdown' do
+ find('.btn-default').click
+ click_link 'version 1'
+ end
+ end
+
+ it 'has a path with comparison context' do
+ expect(page).to have_current_path diffs_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request.iid,
+ diff_id: 2,
+ start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
+ )
+ end
+
+ it 'should have correct value in the compare dropdown' do
+ page.within '.mr-version-compare-dropdown' do
+ expect(page).to have_content 'version 1'
+ end
+ end
+
+ it 'show the message about disabled comments' do
+ expect(page).to have_content 'Comments are disabled'
+ end
+
+ it 'show diff between new and old version' do
+ expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
+ end
+
+ it 'should return to latest version when "Show latest version" button is clicked' do
+ click_link 'Show latest version'
+ page.within '.mr-version-dropdown' do
+ expect(page).to have_content 'latest version'
+ end
+ expect(page).to have_content '8 changed files'
+ end
end
end
diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb
new file mode 100644
index 00000000000..b56fdfe5611
--- /dev/null
+++ b/spec/features/merge_requests/update_merge_requests_spec.rb
@@ -0,0 +1,132 @@
+require 'rails_helper'
+
+feature 'Multiple merge requests updating from merge_requests#index', feature: true do
+ include WaitForAjax
+
+ let!(:user) { create(:user)}
+ let!(:project) { create(:project) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'status', js: true do
+ describe 'close merge request' do
+ before do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ it 'closes merge request' do
+ change_status('Closed')
+
+ expect(page).to have_selector('.merge-request', count: 0)
+ end
+ end
+
+ describe 'reopen merge request' do
+ before do
+ merge_request.close
+ visit namespace_project_merge_requests_path(project.namespace, project, state: 'closed')
+ end
+
+ it 'reopens merge request' do
+ change_status('Open')
+
+ expect(page).to have_selector('.merge-request', count: 0)
+ end
+ end
+ end
+
+ context 'assignee', js: true do
+ describe 'set assignee' do
+ before do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ it "updates merge request with assignee" do
+ change_assignee(user.name)
+
+ page.within('.merge-request .controls') do
+ expect(find('.author_link')["title"]).to have_content(user.name)
+ end
+ end
+ end
+
+ describe 'remove assignee' do
+ before do
+ merge_request.assignee = user
+ merge_request.save
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ it "removes assignee from the merge request" do
+ change_assignee('Unassigned')
+
+ expect(find('.merge-request .controls')).not_to have_css('.author_link')
+ end
+ end
+ end
+
+ context 'milestone', js: true do
+ let(:milestone) { create(:milestone, project: project) }
+
+ describe 'set milestone' do
+ before do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ it "updates merge request with milestone" do
+ change_milestone(milestone.title)
+
+ expect(find('.merge-request')).to have_content milestone.title
+ end
+ end
+
+ describe 'unset milestone' do
+ before do
+ merge_request.milestone = milestone
+ merge_request.save
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ it "removes milestone from the merge request" do
+ change_milestone("No Milestone")
+
+ expect(find('.merge-request')).not_to have_content milestone.title
+ end
+ end
+ end
+
+ def change_status(text)
+ find('#check_all_issues').click
+ find('.js-issue-status').click
+ find('.dropdown-menu-status a', text: text).click
+ click_update_merge_requests_button
+ end
+
+ def change_assignee(text)
+ find('#check_all_issues').click
+ find('.js-update-assignee').click
+ wait_for_ajax
+
+ page.within '.dropdown-menu-user' do
+ click_link text
+ end
+
+ click_update_merge_requests_button
+ end
+
+ def change_milestone(text)
+ find('#check_all_issues').click
+ find('.issues_bulk_update .js-milestone-select').click
+ find('.dropdown-menu-milestone a', text: text).click
+ click_update_merge_requests_button
+ end
+
+ def click_update_merge_requests_button
+ find('.update_selected_issues').click
+ wait_for_ajax
+ end
+end
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index d9ef0d18074..22d9d1b9fd5 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
feature 'Merge Requests > User uses slash commands', feature: true, js: true do
+ include SlashCommandsHelpers
include WaitForAjax
let(:user) { create(:user) }
@@ -20,11 +21,12 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
+ after do
+ wait_for_ajax
+ end
+
it 'does not recognize the command nor create a note' do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/due 2016-08-28"
- click_button 'Comment'
- end
+ write_note("/due 2016-08-28")
expect(page).not_to have_content '/due 2016-08-28'
end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index c43661e5681..b8c838bf7ab 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -3,9 +3,8 @@ require 'rails_helper'
feature 'Milestone', feature: true do
include WaitForAjax
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
- let(:milestone) { create(:milestone, project: project, title: 8.7) }
before do
project.team << [user, :master]
@@ -13,7 +12,7 @@ feature 'Milestone', feature: true do
end
feature 'Create a milestone' do
- scenario 'shows an informative message for a new issue' do
+ scenario 'shows an informative message for a new milestone' do
visit new_namespace_project_milestone_path(project.namespace, project)
page.within '.milestone-form' do
fill_in "milestone_title", with: '8.7'
@@ -26,10 +25,26 @@ feature 'Milestone', feature: true do
feature 'Open a milestone with closed issues' do
scenario 'shows an informative message' do
+ milestone = create(:milestone, project: project, title: 8.7)
+
create(:issue, title: "Bugfix1", project: project, milestone: milestone, state: "closed")
visit namespace_project_milestone_path(project.namespace, project, milestone)
expect(find('.alert-success')).to have_content('All issues for this milestone are closed. You may close this milestone now.')
end
end
+
+ feature 'Open a milestone with an existing title' do
+ scenario 'displays validation message' do
+ milestone = create(:milestone, project: project, title: 8.7)
+
+ visit new_namespace_project_milestone_path(project.namespace, project)
+ page.within '.milestone-form' do
+ fill_in "milestone_title", with: milestone.title
+ end
+ find('input[name="commit"]').click
+
+ expect(find('.alert-danger')).to have_content('Title has already been taken')
+ end
+ end
end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
new file mode 100644
index 00000000000..3b20d38c520
--- /dev/null
+++ b/spec/features/profiles/keys_spec.rb
@@ -0,0 +1,18 @@
+require 'rails_helper'
+
+describe 'Profile > SSH Keys', feature: true do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ visit profile_keys_path
+ end
+
+ describe 'User adds an SSH key' do
+ it 'auto-populates the title', js: true do
+ fill_in('Key', with: attributes_for(:key).fetch(:key))
+
+ expect(find_field('Title').value).to eq 'dummy@gitlab.com'
+ end
+ end
+end
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
index 04058300570..92028c19361 100644
--- a/spec/features/projects/branches/download_buttons_spec.rb
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -33,7 +33,11 @@ feature 'Download buttons in branches page', feature: true do
end
scenario 'shows download artifacts button' do
- expect(page).to have_link "Download '#{build.name}'"
+ href = latest_succeeded_namespace_project_artifacts_path(
+ project.namespace, project, 'binary-encoding/download',
+ job: 'build')
+
+ expect(page).to have_link "Download '#{build.name}'", href: href
end
end
end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 1b14945bf0a..d26a0caf036 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -1,32 +1,46 @@
require 'spec_helper'
describe 'Branches', feature: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
let(:repository) { project.repository }
- before do
- login_as :user
- project.team << [@user, :developer]
- end
+ context 'logged in' do
+ before do
+ login_as :user
+ project.team << [@user, :developer]
+ end
- describe 'Initial branches page' do
- it 'shows all the branches' do
- visit namespace_project_branches_path(project.namespace, project)
+ describe 'Initial branches page' do
+ it 'shows all the branches' do
+ visit namespace_project_branches_path(project.namespace, project)
- repository.branches { |branch| expect(page).to have_content("#{branch.name}") }
- expect(page).to have_content("Protected branches can be managed in project settings")
+ repository.branches { |branch| expect(page).to have_content("#{branch.name}") }
+ expect(page).to have_content("Protected branches can be managed in project settings")
+ end
+ end
+
+ describe 'Find branches' do
+ it 'shows filtered branches', js: true do
+ visit namespace_project_branches_path(project.namespace, project)
+
+ fill_in 'branch-search', with: 'fix'
+ find('#branch-search').native.send_keys(:enter)
+
+ expect(page).to have_content('fix')
+ expect(find('.all-branches')).to have_selector('li', count: 1)
+ end
end
end
- describe 'Find branches' do
- it 'shows filtered branches', js: true do
+ context 'logged out' do
+ before do
visit namespace_project_branches_path(project.namespace, project)
+ end
- fill_in 'branch-search', with: 'fix'
- find('#branch-search').native.send_keys(:enter)
-
- expect(page).to have_content('fix')
- expect(find('.all-branches')).to have_selector('li', count: 1)
+ it 'does not show merge request button' do
+ page.within first('.all-branches li') do
+ expect(page).not_to have_content 'Merge Request'
+ end
end
end
end
diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb
index 0cfeb2e57d8..d1685f95503 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+require 'tempfile'
describe "Builds" do
let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
@@ -6,7 +7,7 @@ describe "Builds" do
before do
login_as(:user)
@commit = FactoryGirl.create :ci_pipeline
- @build = FactoryGirl.create :ci_build, pipeline: @commit
+ @build = FactoryGirl.create :ci_build, :trace, pipeline: @commit
@build2 = FactoryGirl.create :ci_build
@project = @commit.project
@project.team << [@user, :developer]
@@ -156,7 +157,6 @@ describe "Builds" do
context 'Build raw trace' do
before do
@build.run!
- @build.trace = 'BUILD TRACE'
visit namespace_project_build_path(@project.namespace, @project, @build)
end
@@ -164,6 +164,26 @@ describe "Builds" do
expect(page).to have_link 'Raw'
end
end
+
+ describe 'Variables' do
+ before do
+ @trigger_request = create :ci_trigger_request_with_variables
+ @build = create :ci_build, pipeline: @commit, trigger_request: @trigger_request
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ it 'shows variable key and value after click', js: true do
+ expect(page).to have_css('.reveal-variables')
+ expect(page).not_to have_css('.js-build-variable')
+ expect(page).not_to have_css('.js-build-value')
+
+ click_button 'Reveal Variables'
+
+ expect(page).not_to have_css('.reveal-variables')
+ expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
+ expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
+ end
+ end
end
describe "POST /:project/builds/:id/cancel" do
@@ -255,35 +275,101 @@ describe "Builds" do
end
end
- describe "GET /:project/builds/:id/raw" do
- context "Build from project" do
- before do
- Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build.run!
- @build.trace = 'BUILD TRACE'
- visit namespace_project_build_path(@project.namespace, @project, @build)
- page.within('.js-build-sidebar') { click_link 'Raw' }
+ describe 'GET /:project/builds/:id/raw' do
+ context 'access source' do
+ context 'build from project' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ end
end
- it 'sends the right headers' do
- expect(page.status_code).to eq(200)
- expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ context 'build from other project' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build2.run!
+ visit raw_namespace_project_build_path(@project.namespace, @project, @build2)
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(404)
+ end
end
end
- context "Build from other project" do
- before do
- Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build2.run!
- @build2.trace = 'BUILD TRACE'
- visit raw_namespace_project_build_path(@project.namespace, @project, @build2)
- puts page.status_code
- puts current_url
+ context 'storage form' do
+ let(:existing_file) { Tempfile.new('existing-trace-file').path }
+ let(:non_existing_file) do
+ file = Tempfile.new('non-existing-trace-file')
+ path = file.path
+ file.unlink
+ path
end
- it 'sends the right headers' do
- expect(page.status_code).to eq(404)
+ context 'when build has trace in file' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+
+ allow_any_instance_of(Project).to receive(:ci_id).and_return(nil)
+ allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(existing_file)
+ allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(non_existing_file)
+
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(existing_file)
+ end
+ end
+
+ context 'when build has trace in old file' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+
+ allow_any_instance_of(Project).to receive(:ci_id).and_return(999)
+ allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file)
+ allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(existing_file)
+
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(existing_file)
+ end
+ end
+
+ context 'when build has trace in DB' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+
+ allow_any_instance_of(Project).to receive(:ci_id).and_return(nil)
+ allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file)
+ allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(non_existing_file)
+
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(404)
+ end
end
end
end
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
new file mode 100644
index 00000000000..a1643fd1f43
--- /dev/null
+++ b/spec/features/projects/edit_spec.rb
@@ -0,0 +1,57 @@
+require 'rails_helper'
+
+feature 'Project edit', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ context 'feature visibility' do
+ context 'merge requests select' do
+ it 'hides merge requests section' do
+ select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level')
+
+ expect(page).to have_selector('.merge-requests-feature', visible: false)
+ end
+
+ it 'hides merge requests section after save' do
+ select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level')
+
+ expect(page).to have_selector('.merge-requests-feature', visible: false)
+
+ click_button 'Save changes'
+
+ wait_for_ajax
+
+ expect(page).to have_selector('.merge-requests-feature', visible: false)
+ end
+ end
+
+ context 'builds select' do
+ it 'hides merge requests section' do
+ select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
+
+ expect(page).to have_selector('.builds-feature', visible: false)
+ end
+
+ it 'hides merge requests section after save' do
+ select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
+
+ expect(page).to have_selector('.builds-feature', visible: false)
+
+ click_button 'Save changes'
+
+ wait_for_ajax
+
+ expect(page).to have_selector('.builds-feature', visible: false)
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb
index be5cebcd7c9..d7c29a7e074 100644
--- a/spec/features/projects/files/download_buttons_spec.rb
+++ b/spec/features/projects/files/download_buttons_spec.rb
@@ -34,7 +34,11 @@ feature 'Download buttons in files tree', feature: true do
end
scenario 'shows download artifacts button' do
- expect(page).to have_link "Download '#{build.name}'"
+ href = latest_succeeded_namespace_project_artifacts_path(
+ project.namespace, project, "#{project.default_branch}/download",
+ job: 'build')
+
+ expect(page).to have_link "Download '#{build.name}'", href: href
end
end
end
diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
new file mode 100644
index 00000000000..012befa7990
--- /dev/null
+++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+feature 'User uses soft wrap whilst editing file', feature: true, js: true do
+ before do
+ user = create(:user)
+ project = create(:project)
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'test_file-name')
+ editor = find('.file-editor.code')
+ editor.click
+ editor.send_keys 'Touch water with paw then recoil in horror chase dog then
+ run away chase the pig around the house eat owner\'s food, and knock
+ dish off table head butt cant eat out of my own dish. Cat is love, cat
+ is life rub face on everything poop on grasses so meow. Playing with
+ balls of wool flee in terror at cucumber discovered on floor run in
+ circles tuxedo cats always looking dapper, but attack dog, run away
+ and pretend to be victim so all of a sudden cat goes crazy, yet chase
+ laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn
+ hanging out of own butt jump off balcony, onto stranger\'s head yet
+ chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish
+ end
+
+ let(:toggle_button) { find('.soft-wrap-toggle') }
+
+ scenario 'user clicks the "Soft wrap" button and then "No wrap" button' do
+ wrapped_content_width = get_content_width
+ toggle_button.click
+ expect(toggle_button).to have_content 'No wrap'
+ unwrapped_content_width = get_content_width
+ expect(unwrapped_content_width).to be < wrapped_content_width
+
+ toggle_button.click
+ expect(toggle_button).to have_content 'Soft wrap'
+ expect(get_content_width).to be > unwrapped_content_width
+ end
+
+ def get_content_width
+ find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/)
+ end
+end
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
new file mode 100644
index 00000000000..1921ea6d8ae
--- /dev/null
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'GFM autocomplete loading', feature: true, js: true do
+ let(:project) { create(:project) }
+
+ before do
+ login_as :admin
+
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'does not load on project#show' do
+ expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('')
+ end
+
+ it 'loads on new issue page' do
+ visit new_namespace_project_issue_path(project.namespace, project)
+
+ expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('')
+ end
+end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
new file mode 100644
index 00000000000..27c986c5187
--- /dev/null
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+# Integration test that exports a file using the Import/Export feature
+# It looks up for any sensitive word inside the JSON, so if a sensitive word is found
+# we''l have to either include it adding the model that includes it to the +safe_list+
+# or make sure the attribute is blacklisted in the +import_export.yml+ configuration
+feature 'Import/Export - project export integration test', feature: true, js: true do
+ include Select2Helper
+ include ExportFileHelper
+
+ let(:user) { create(:admin) }
+ let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+ let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+
+ let(:sensitive_words) { %w[pass secret token key] }
+ let(:safe_list) do
+ {
+ token: [ProjectHook, Ci::Trigger, CommitStatus],
+ key: [Project, Ci::Variable, :yaml_variables]
+ }
+ end
+ let(:safe_hashes) { { yaml_variables: %w[key value public] } }
+
+ let(:project) { setup_project }
+
+ background do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path, secure: true)
+ end
+
+ context 'admin user' do
+ before do
+ login_as(user)
+ end
+
+ scenario 'exports a project successfully' do
+ visit edit_namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content('Export project')
+
+ click_link 'Export project'
+
+ visit edit_namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content('Download export')
+
+ in_directory_with_expanded_export(project) do |exit_status, tmpdir|
+ expect(exit_status).to eq(0)
+
+ project_json_path = File.join(tmpdir, 'project.json')
+ expect(File).to exist(project_json_path)
+
+ project_hash = JSON.parse(IO.read(project_json_path))
+
+ sensitive_words.each do |sensitive_word|
+ found = find_sensitive_attributes(sensitive_word, project_hash)
+
+ expect(found).to be_nil, failure_message(found.try(:key_found), found.try(:parent), sensitive_word)
+ end
+ end
+ end
+
+ def failure_message(key_found, parent, sensitive_word)
+ <<-MSG
+ Found a new sensitive word <#{key_found}>, which is part of the hash #{parent.inspect}
+
+ If you think this information shouldn't get exported, please exclude the model or attribute in IMPORT_EXPORT_CONFIG.
+
+ Otherwise, please add the exception to +safe_list+ in CURRENT_SPEC using #{sensitive_word} as the key and the
+ correspondent hash or model as the value.
+
+ IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
+ CURRENT_SPEC: #{__FILE__}
+ MSG
+ end
+ end
+end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index f707ccf4e93..09cd6369881 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project import', feature: true, js: true do
+feature 'Import/Export - project import integration test', feature: true, js: true do
include Select2Helper
let(:admin) { create(:admin) }
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index e14b2705704..d04bdea0fe4 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 4a83740621a..f76c4fe8b57 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -13,10 +13,12 @@ feature 'issuable templates', feature: true, js: true do
context 'user creates an issue using templates' do
let(:template_content) { 'this is a test "bug" template' }
+ let(:longtemplate_content) { %Q(this\n\n\n\n\nis\n\n\n\n\na\n\n\n\n\nbug\n\n\n\n\ntemplate) }
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
background do
project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+ project.repository.commit_file(user, '.gitlab/issue_templates/test.md', longtemplate_content, 'added issue template', 'master', false)
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
end
@@ -27,6 +29,17 @@ feature 'issuable templates', feature: true, js: true do
preview_template
save_changes
end
+
+ it 'updates height of markdown textarea' do
+ start_height = page.evaluate_script('$(".markdown-area").outerHeight()')
+
+ select_template 'test'
+ wait_for_ajax
+
+ end_height = page.evaluate_script('$(".markdown-area").outerHeight()')
+
+ expect(end_height).not_to eq(start_height)
+ end
end
context 'user creates a merge request using templates' do
@@ -51,7 +64,7 @@ feature 'issuable templates', feature: true, js: true do
let(:template_content) { 'this is a test "feature-proposal" template' }
let(:fork_user) { create(:user) }
let(:fork_project) { create(:project, :public) }
- let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project) }
+ let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project, target_project: project) }
background do
logout
@@ -59,16 +72,20 @@ feature 'issuable templates', feature: true, js: true do
fork_project.team << [fork_user, :master]
create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
login_as fork_user
- fork_project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
- visit edit_namespace_project_merge_request_path fork_project.namespace, fork_project, merge_request
+ project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
- scenario 'user selects "feature-proposal" template' do
- select_template 'feature-proposal'
- wait_for_ajax
- preview_template
- save_changes
+ context 'feature proposal template' do
+ context 'template exists in target project' do
+ scenario 'user selects template' do
+ select_template 'feature-proposal'
+ wait_for_ajax
+ preview_template
+ save_changes
+ end
+ end
end
end
diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb
index b26c0ea7a14..227ccf9459c 100644
--- a/spec/features/projects/main/download_buttons_spec.rb
+++ b/spec/features/projects/main/download_buttons_spec.rb
@@ -33,7 +33,11 @@ feature 'Download buttons in project main page', feature: true do
end
scenario 'shows download artifacts button' do
- expect(page).to have_link "Download '#{build.name}'"
+ href = latest_succeeded_namespace_project_artifacts_path(
+ project.namespace, project, "#{project.default_branch}/download",
+ job: 'build')
+
+ expect(page).to have_link "Download '#{build.name}'", href: href
end
end
end
diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
index 67811b1048e..6e948b7a616 100644
--- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
@@ -1,12 +1,10 @@
require 'spec_helper'
feature 'Projects > Members > Owner cannot leave project', feature: true do
- let(:owner) { create(:user) }
let(:project) { create(:project) }
background do
- project.team << [owner, :owner]
- login_as(owner)
+ login_as(project.owner)
visit namespace_project_path(project.namespace, project)
end
diff --git a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
index 0e54c4fdf20..4ca9272b9c1 100644
--- a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
@@ -1,12 +1,10 @@
require 'spec_helper'
feature 'Projects > Members > Owner cannot request access to his project', feature: true do
- let(:owner) { create(:user) }
let(:project) { create(:project) }
background do
- project.team << [owner, :owner]
- login_as(owner)
+ login_as(project.owner)
visit namespace_project_path(project.namespace, project)
end
diff --git a/spec/features/projects/snippets_spec.rb b/spec/features/projects/snippets_spec.rb
new file mode 100644
index 00000000000..d37e8ed4699
--- /dev/null
+++ b/spec/features/projects/snippets_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe 'Project snippets', feature: true do
+ context 'when the project has snippets' do
+ let(:project) { create(:empty_project, :public) }
+ let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+ visit namespace_project_snippets_path(project.namespace, project)
+ end
+
+ it_behaves_like 'paginated snippets'
+ end
+end
diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb
index 6e0022c179f..dd93d25c2c6 100644
--- a/spec/features/projects/tags/download_buttons_spec.rb
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -34,7 +34,11 @@ feature 'Download buttons in tags page', feature: true do
end
scenario 'shows download artifacts button' do
- expect(page).to have_link "Download '#{build.name}'"
+ href = latest_succeeded_namespace_project_artifacts_path(
+ project.namespace, project, "#{tag}/download",
+ job: 'build')
+
+ expect(page).to have_link "Download '#{build.name}'", href: href
end
end
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index e00d85904d5..c30d38b6508 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -57,7 +57,7 @@ feature 'Project', feature: true do
describe 'removal', js: true do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, namespace: user.namespace, name: 'project1') }
before do
login_with(user)
@@ -65,8 +65,12 @@ feature 'Project', feature: true do
visit edit_namespace_project_path(project.namespace, project)
end
- it 'removes project' do
+ it 'removes a project' do
expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1)
+ expect(page).to have_content "Project 'project1' will be deleted."
+ expect(Project.all.count).to be_zero
+ expect(project.issues).to be_empty
+ expect(project.merge_requests).to be_empty
end
end
@@ -78,7 +82,7 @@ feature 'Project', feature: true do
before do
login_with(user)
- project.team.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MASTER)
visit namespace_project_path(project.namespace, project)
end
@@ -97,8 +101,8 @@ feature 'Project', feature: true do
context 'on issues page', js: true do
before do
login_with(user)
- project.team.add_user(user, Gitlab::Access::MASTER)
- project2.team.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MASTER)
+ project2.add_user(user, Gitlab::Access::MASTER)
visit namespace_project_issue_path(project.namespace, project, issue)
end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index a5ed3595b0a..0e1cc9a0f73 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -60,7 +60,7 @@ describe "Runners" do
it "removes specific runner for project if this is last project for that runners" do
within ".activated-specific-runners" do
- click_on "Remove runner"
+ click_on "Remove Runner"
end
expect(Ci::Runner.exists?(id: @specific_runner)).to be_falsey
@@ -75,7 +75,7 @@ describe "Runners" do
end
it "enables shared runners" do
- click_on "Enable shared runners"
+ click_on "Enable shared Runners"
expect(@project.reload.shared_runners_enabled).to be_truthy
end
end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index dcd3a2f17b0..1806200c82c 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe "Search", feature: true do
+ include WaitForAjax
+
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let!(:issue) { create(:issue, project: project, assignee: user) }
@@ -16,6 +18,36 @@ describe "Search", feature: true do
expect(page).not_to have_selector('.search')
end
+ context 'search filters', js: true do
+ let(:group) { create(:group) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ it 'shows group name after filtering' do
+ find('.js-search-group-dropdown').click
+ wait_for_ajax
+
+ page.within '.search-holder' do
+ click_link group.name
+ end
+
+ expect(find('.js-search-group-dropdown')).to have_content(group.name)
+ end
+
+ it 'shows project name after filtering' do
+ page.within('.project-filter') do
+ find('.js-search-project-dropdown').click
+ wait_for_ajax
+
+ click_link project.name_with_namespace
+ end
+
+ expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace)
+ end
+ end
+
describe 'searching for Projects' do
it 'finds a project' do
page.within '.search-holder' do
diff --git a/spec/features/snippets_spec.rb b/spec/features/snippets_spec.rb
new file mode 100644
index 00000000000..70b16bfc810
--- /dev/null
+++ b/spec/features/snippets_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe 'Snippets', feature: true do
+ context 'when the project has snippets' do
+ let(:project) { create(:empty_project, :public) }
+ let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+ visit snippets_path(username: project.owner.username)
+ end
+
+ it_behaves_like 'paginated snippets'
+ end
+end
diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb
index 83cf306437d..b9e66243d84 100644
--- a/spec/features/todos/todos_filtering_spec.rb
+++ b/spec/features/todos/todos_filtering_spec.rb
@@ -29,8 +29,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
fill_in 'Search projects', with: project_1.name_with_namespace
click_link project_1.name_with_namespace
end
+
wait_for_ajax
- expect('.prepend-top-default').not_to have_content project_2.name_with_namespace
+
+ expect(page).to have_content project_1.name_with_namespace
+ expect(page).not_to have_content project_2.name_with_namespace
end
it 'filters by author' do
@@ -39,8 +42,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
fill_in 'Search authors', with: user_1.name
click_link user_1.name
end
+
wait_for_ajax
- expect('.prepend-top-default').not_to have_content user_2.name
+
+ expect(find('.todos-list')).to have_content user_1.name
+ expect(find('.todos-list')).not_to have_content user_2.name
end
it 'filters by type' do
@@ -48,8 +54,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
within '.dropdown-menu-type' do
click_link 'Issue'
end
+
wait_for_ajax
- expect('.prepend-top-default').not_to have_content ' merge request !'
+
+ expect(find('.todos-list')).to have_content issue.to_reference
+ expect(find('.todos-list')).not_to have_content merge_request.to_reference
end
it 'filters by action' do
@@ -57,7 +66,10 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
within '.dropdown-menu-action' do
click_link 'Assigned'
end
+
wait_for_ajax
- expect('.prepend-top-default').not_to have_content ' mentioned '
+
+ expect(find('.todos-list')).to have_content ' assigned you '
+ expect(find('.todos-list')).not_to have_content ' mentioned '
end
end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 3cbc8253ad6..72354834c5a 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -12,7 +12,7 @@ describe 'Triggers' do
context 'create a trigger' do
before do
- click_on 'Add Trigger'
+ click_on 'Add trigger'
expect(@project.triggers.count).to eq(1)
end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index a46e48c76ed..ff6933dc8d9 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -156,6 +156,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
describe "when 2FA via OTP is disabled" do
it "allows logging in with the U2F device" do
+ user.update_attribute(:otp_required_for_login, false)
login_with(user)
@u2f_device.respond_to_u2f_authentication
@@ -181,6 +182,19 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end
end
+ it 'persists remember_me value via hidden field' do
+ login_with(user, remember: true)
+
+ @u2f_device.respond_to_u2f_authentication
+ click_on "Login Via U2F Device"
+ expect(page.body).to match('We heard back from your U2F device')
+
+ within 'div#js-authenticate-u2f' do
+ field = first('input#user_remember_me', visible: false)
+ expect(field.value).to eq '1'
+ end
+ end
+
describe "when a given U2F device has already been registered by another user" do
describe "but not the current user" do
it "does not allow logging in with that particular device" do
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
new file mode 100644
index 00000000000..33b52d1547e
--- /dev/null
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe 'Unsubscribe links', feature: true do
+ include Warden::Test::Helpers
+
+ let(:recipient) { create(:user) }
+ let(:author) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+ let(:params) { { title: 'A bug!', description: 'Fix it!', assignee: recipient } }
+ let(:issue) { Issues::CreateService.new(project, author, params).execute }
+
+ let(:mail) { ActionMailer::Base.deliveries.last }
+ let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) }
+ let(:header_link) { mail.header['List-Unsubscribe'].to_s[1..-2] } # Strip angle brackets
+ let(:body_link) { body.find_link('unsubscribe')['href'] }
+
+ before do
+ perform_enqueued_jobs { issue }
+ end
+
+ context 'when logged out' do
+ context 'when visiting the link from the body' do
+ it 'shows the unsubscribe confirmation page and redirects to root path when confirming' do
+ visit body_link
+
+ expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
+ expect(page).to have_text(%(Unsubscribe from issue #{issue.title} (#{issue.to_reference})))
+ expect(page).to have_text(%(Are you sure you want to unsubscribe from issue #{issue.title} (#{issue.to_reference})?))
+ expect(issue.subscribed?(recipient)).to be_truthy
+
+ click_link 'Unsubscribe'
+
+ expect(issue.subscribed?(recipient)).to be_falsey
+ expect(current_path).to eq new_user_session_path
+ end
+
+ it 'shows the unsubscribe confirmation page and redirects to root path when canceling' do
+ visit body_link
+
+ expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
+ expect(issue.subscribed?(recipient)).to be_truthy
+
+ click_link 'Cancel'
+
+ expect(issue.subscribed?(recipient)).to be_truthy
+ expect(current_path).to eq new_user_session_path
+ end
+ end
+
+ it 'unsubscribes from the issue when visiting the link from the header' do
+ visit header_link
+
+ expect(page).to have_text('unsubscribed')
+ expect(issue.subscribed?(recipient)).to be_falsey
+ end
+ end
+
+ context 'when logged in' do
+ before { login_as(recipient) }
+
+ it 'unsubscribes from the issue when visiting the link from the email body' do
+ visit body_link
+
+ expect(page).to have_text('unsubscribed')
+ expect(issue.subscribed?(recipient)).to be_falsey
+ end
+
+ it 'unsubscribes from the issue when visiting the link from the header' do
+ visit header_link
+
+ expect(page).to have_text('unsubscribed')
+ expect(issue.subscribed?(recipient)).to be_falsey
+ end
+ end
+end
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
new file mode 100644
index 00000000000..ce7e809ec76
--- /dev/null
+++ b/spec/features/users/snippets_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe 'Snippets tab on a user profile', feature: true, js: true do
+ include WaitForAjax
+
+ context 'when the user has snippets' do
+ let(:user) { create(:user) }
+ let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+ end
+
+ it_behaves_like 'paginated snippets', remote: true
+ end
+end
diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb
new file mode 100644
index 00000000000..6cc90299417
--- /dev/null
+++ b/spec/finders/access_requests_finder_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe AccessRequestsFinder, services: true do
+ let(:user) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:project) { create(:project) }
+ let(:group) { create(:group) }
+
+ before do
+ project.request_access(access_requester)
+ group.request_access(access_requester)
+ end
+
+ shared_examples 'a finder returning access requesters' do |method_name|
+ it 'returns access requesters' do
+ access_requesters = described_class.new(source).public_send(method_name, user)
+
+ expect(access_requesters.size).to eq(1)
+ expect(access_requesters.first).to be_a "#{source.class.to_s}Member".constantize
+ expect(access_requesters.first.user).to eq(access_requester)
+ end
+ end
+
+ shared_examples 'a finder returning no results' do |method_name|
+ it 'raises Gitlab::Access::AccessDeniedError' do
+ expect(described_class.new(source).public_send(method_name, user)).to be_empty
+ end
+ end
+
+ shared_examples 'a finder raising Gitlab::Access::AccessDeniedError' do |method_name|
+ it 'raises Gitlab::Access::AccessDeniedError' do
+ expect { described_class.new(source).public_send(method_name, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ describe '#execute' do
+ context 'when current user cannot see project access requests' do
+ it_behaves_like 'a finder returning no results', :execute do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a finder returning no results', :execute do
+ let(:source) { group }
+ end
+ end
+
+ context 'when current user can see access requests' do
+ before do
+ project.team << [user, :master]
+ group.add_owner(user)
+ end
+
+ it_behaves_like 'a finder returning access requesters', :execute do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a finder returning access requesters', :execute do
+ let(:source) { group }
+ end
+ end
+ end
+
+ describe '#execute!' do
+ context 'when current user cannot see access requests' do
+ it_behaves_like 'a finder raising Gitlab::Access::AccessDeniedError', :execute! do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a finder raising Gitlab::Access::AccessDeniedError', :execute! do
+ let(:source) { group }
+ end
+ end
+
+ context 'when current user can see access requests' do
+ before do
+ project.team << [user, :master]
+ group.add_owner(user)
+ end
+
+ it_behaves_like 'a finder returning access requesters', :execute! do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a finder returning access requesters', :execute! do
+ let(:source) { group }
+ end
+ end
+ end
+end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index ec8809e6926..40bccb8e50b 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -7,8 +7,8 @@ describe IssuesFinder do
let(:project2) { create(:empty_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(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') }
+ let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') }
let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) }
let!(:label_link) { create(:label_link, label: label, target: issue2) }
@@ -127,6 +127,22 @@ describe IssuesFinder do
end
end
+ context 'filtering by issue term' do
+ let(:params) { { search: 'git' } }
+
+ it 'returns issues with title and description match for search term' do
+ expect(issues).to contain_exactly(issue1, issue2)
+ end
+ end
+
+ context 'filtering by issue iid' do
+ let(:params) { { search: issue3.to_reference } }
+
+ it 'returns issue with iid match' do
+ expect(issues).to contain_exactly(issue3)
+ end
+ end
+
context 'when the user is unauthorized' do
let(:search_user) { nil }
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
index f90a8e007c8..29a47e005a6 100644
--- a/spec/finders/joined_groups_finder_spec.rb
+++ b/spec/finders/joined_groups_finder_spec.rb
@@ -43,7 +43,7 @@ describe JoinedGroupsFinder do
context 'if profile visitor is in one of the private group projects' do
before do
project = create(:project, :private, group: private_group, name: 'B', path: 'B')
- project.team.add_user(profile_visitor, Gitlab::Access::DEVELOPER)
+ project.add_user(profile_visitor, Gitlab::Access::DEVELOPER)
end
it 'shows group' do
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
new file mode 100644
index 00000000000..b0811d134fa
--- /dev/null
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe PipelinesFinder do
+ let(:project) { create(:project) }
+
+ let!(:tag_pipeline) { create(:ci_pipeline, project: project, ref: 'v1.0.0') }
+ let!(:branch_pipeline) { create(:ci_pipeline, project: project) }
+
+ subject { described_class.new(project).execute(params) }
+
+ describe "#execute" do
+ context 'when a scope is passed' do
+ context 'when scope is nil' do
+ let(:params) { { scope: nil } }
+
+ it 'selects all pipelines' do
+ expect(subject.count).to be 2
+ expect(subject).to include tag_pipeline
+ expect(subject).to include branch_pipeline
+ end
+ end
+
+ context 'when selecting branches' do
+ let(:params) { { scope: 'branches' } }
+
+ it 'excludes tags' do
+ expect(subject).not_to include tag_pipeline
+ expect(subject).to include branch_pipeline
+ end
+ end
+
+ context 'when selecting tags' do
+ let(:params) { { scope: 'tags' } }
+
+ it 'excludes branches' do
+ expect(subject).to include tag_pipeline
+ expect(subject).not_to include branch_pipeline
+ end
+ end
+ end
+
+ # Scoping to running will speed up the test as it doesn't hit the FS
+ let(:params) { { scope: 'running' } }
+
+ it 'orders in descending order on ID' do
+ feature_pipeline = create(:ci_pipeline, project: project, ref: 'feature')
+
+ expected_ids = [feature_pipeline.id, branch_pipeline.id, tag_pipeline.id].sort.reverse
+ expect(subject.map(&:id)).to eq expected_ids
+ end
+ end
+end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 7a3a74335e8..13bda5f7c5a 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -38,7 +38,7 @@ describe ProjectsFinder do
describe 'with private projects' do
before do
- private_project.team.add_user(user, Gitlab::Access::MASTER)
+ private_project.add_user(user, Gitlab::Access::MASTER)
end
it do
diff --git a/spec/helpers/git_helper_spec.rb b/spec/helpers/git_helper_spec.rb
new file mode 100644
index 00000000000..9b1ef1e05a2
--- /dev/null
+++ b/spec/helpers/git_helper_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe GitHelper do
+ describe '#short_sha' do
+ let(:short_sha) { helper.short_sha('d4e043f6c20749a3ab3f4b8e23f2a8979f4b9100') }
+
+ it { expect(short_sha).to eq('d4e043f6') }
+ end
+end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 0807534720a..233d00534e5 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -18,4 +18,67 @@ describe GroupsHelper do
expect(group_icon(group.path)).to match('group_avatar.png')
end
end
+
+ describe 'group_lfs_status' do
+ let(:group) { create(:group) }
+ let!(:project) { create(:empty_project, namespace_id: group.id) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ context 'only one project in group' do
+ before do
+ group.update_attribute(:lfs_enabled, true)
+ end
+
+ it 'returns all projects as enabled' do
+ expect(group_lfs_status(group)).to include('Enabled for all projects')
+ end
+
+ it 'returns all projects as disabled' do
+ project.update_attribute(:lfs_enabled, false)
+
+ expect(group_lfs_status(group)).to include('Enabled for 0 out of 1 project')
+ end
+ end
+
+ context 'more than one project in group' do
+ before do
+ create(:empty_project, namespace_id: group.id)
+ end
+
+ context 'LFS enabled in group' do
+ before do
+ group.update_attribute(:lfs_enabled, true)
+ end
+
+ it 'returns both projects as enabled' do
+ expect(group_lfs_status(group)).to include('Enabled for all projects')
+ end
+
+ it 'returns only one as enabled' do
+ project.update_attribute(:lfs_enabled, false)
+
+ expect(group_lfs_status(group)).to include('Enabled for 1 out of 2 projects')
+ end
+ end
+
+ context 'LFS disabled in group' do
+ before do
+ group.update_attribute(:lfs_enabled, false)
+ end
+
+ it 'returns both projects as disabled' do
+ expect(group_lfs_status(group)).to include('Disabled for all projects')
+ end
+
+ it 'returns only one as disabled' do
+ project.update_attribute(:lfs_enabled, true)
+
+ expect(group_lfs_status(group)).to include('Disabled for 1 out of 2 projects')
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb
index 3391234e9f5..187b891b927 100644
--- a/spec/helpers/import_helper_spec.rb
+++ b/spec/helpers/import_helper_spec.rb
@@ -1,6 +1,30 @@
require 'rails_helper'
describe ImportHelper do
+ describe '#import_project_target' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ context 'when current user can create namespaces' do
+ it 'returns project namespace' do
+ user.update_attribute(:can_create_group, true)
+
+ expect(helper.import_project_target('asd', 'vim')).to eq 'asd/vim'
+ end
+ end
+
+ context 'when current user can not create namespaces' do
+ it "takes the current user's namespace" do
+ user.update_attribute(:can_create_group, false)
+
+ expect(helper.import_project_target('asd', 'vim')).to eq "#{user.namespace_path}/vim"
+ end
+ end
+ end
+
describe '#github_project_link' do
context 'when provider does not specify a custom URL' do
it 'uses default GitHub URL' do
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 2dd2eab0524..62cc10f579a 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
-describe IssuablesHelper do
+describe IssuablesHelper do
let(:label) { build_stubbed(:label) }
let(:label2) { build_stubbed(:label) }
- context 'label tooltip' do
+ describe '#issuable_labels_tooltip' do
it 'returns label text' do
expect(issuable_labels_tooltip([label])).to eq(label.title)
end
@@ -13,4 +13,105 @@ describe IssuablesHelper do
expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
end
end
+
+ describe '#issuables_state_counter_text' do
+ let(:user) { create(:user) }
+
+ describe 'state text' do
+ before do
+ allow(helper).to receive(:issuables_count_for_state).and_return(42)
+ end
+
+ it 'returns "Open" when state is :opened' do
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+ end
+
+ it 'returns "Closed" when state is :closed' do
+ expect(helper.issuables_state_counter_text(:issues, :closed)).
+ to eq('<span>Closed</span> <span class="badge">42</span>')
+ end
+
+ it 'returns "Merged" when state is :merged' do
+ expect(helper.issuables_state_counter_text(:merge_requests, :merged)).
+ to eq('<span>Merged</span> <span class="badge">42</span>')
+ end
+
+ it 'returns "All" when state is :all' do
+ expect(helper.issuables_state_counter_text(:merge_requests, :all)).
+ to eq('<span>All</span> <span class="badge">42</span>')
+ end
+ end
+
+ describe 'counter caching based on issuable type and params', :caching do
+ let(:params) do
+ {
+ scope: 'created-by-me',
+ state: 'opened',
+ utf8: '✓',
+ author_id: '11',
+ assignee_id: '18',
+ label_name: ['bug', 'discussion', 'documentation'],
+ milestone_title: 'v4.0',
+ sort: 'due_date_asc',
+ namespace_id: 'gitlab-org',
+ project_id: 'gitlab-ce',
+ page: 2
+ }.with_indifferent_access
+ end
+
+ it 'returns the cached value when called for the same issuable type & with the same params' do
+ expect(helper).to receive(:params).twice.and_return(params)
+ expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+
+ expect(helper).not_to receive(:issuables_count_for_state)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+ end
+
+ it 'does not take some keys into account in the cache key' do
+ expect(helper).to receive(:params).and_return({
+ author_id: '11',
+ state: 'foo',
+ sort: 'foo',
+ utf8: 'foo',
+ page: 'foo'
+ }.with_indifferent_access)
+ expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+
+ expect(helper).to receive(:params).and_return({
+ author_id: '11',
+ state: 'bar',
+ sort: 'bar',
+ utf8: 'bar',
+ page: 'bar'
+ }.with_indifferent_access)
+ expect(helper).not_to receive(:issuables_count_for_state)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+ end
+
+ it 'does not take params order into account in the cache key' do
+ expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened')
+ expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+
+ expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11')
+ expect(helper).not_to receive(:issuables_count_for_state)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+ end
+ end
+ end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 7998209b7b0..6703d88e357 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -11,7 +11,7 @@ describe MembersHelper do
describe '#remove_member_message' do
let(:requester) { build(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project, :public) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
let(:project_member_request) { project.request_access(requester) }
@@ -32,7 +32,7 @@ describe MembersHelper do
describe '#remove_member_title' do
let(:requester) { build(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project, :public) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_request) { project.request_access(requester) }
let(:group) { create(:group) }
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
new file mode 100644
index 00000000000..28c2268f8d0
--- /dev/null
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe MilestonesHelper do
+ describe '#milestone_counts' do
+ let(:project) { FactoryGirl.create(:project) }
+ let(:counts) { helper.milestone_counts(project.milestones) }
+
+ context 'when there are milestones' do
+ let!(:milestone_1) { FactoryGirl.create(:active_milestone, project: project) }
+ let!(:milestone_2) { FactoryGirl.create(:active_milestone, project: project) }
+ let!(:milestone_3) { FactoryGirl.create(:closed_milestone, project: project) }
+
+ it 'returns the correct counts' do
+ expect(counts).to eq(opened: 2, closed: 1, all: 3)
+ end
+ end
+
+ context 'when there are only milestones of one type' do
+ let!(:milestone_1) { FactoryGirl.create(:active_milestone, project: project) }
+ let!(:milestone_2) { FactoryGirl.create(:active_milestone, project: project) }
+
+ it 'returns the correct counts' do
+ expect(counts).to eq(opened: 2, closed: 0, all: 2)
+ end
+ end
+
+ context 'when there are no milestones' do
+ it 'returns the correct counts' do
+ expect(counts).to eq(opened: 0, closed: 0, all: 0)
+ end
+ end
+ end
+end
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
deleted file mode 100644
index e4d18d8bfc6..00000000000
--- a/spec/helpers/nav_helper_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-require 'spec_helper'
-
-# Specs in this file have access to a helper object that includes
-# the NavHelper. For example:
-#
-# describe NavHelper do
-# describe "string concat" do
-# it "concats two strings with spaces" do
-# expect(helper.concat_strings("this","that")).to eq("this that")
-# end
-# end
-# end
-describe NavHelper do
- describe '#nav_menu_collapsed?' do
- it 'returns true when the nav is collapsed in the cookie' do
- helper.request.cookies[:collapsed_nav] = 'true'
- expect(helper.nav_menu_collapsed?).to eq true
- end
-
- it 'returns false when the nav is not collapsed in the cookie' do
- helper.request.cookies[:collapsed_nav] = 'false'
- expect(helper.nav_menu_collapsed?).to eq false
- end
- end
-end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 284b58d8d5c..70032e7df94 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -174,4 +174,48 @@ describe ProjectsHelper do
end
end
end
+
+ describe "#project_feature_access_select" do
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+
+ context "when project is internal or public" do
+ it "shows all options" do
+ helper.instance_variable_set(:@project, project)
+ result = helper.project_feature_access_select(:issues_access_level)
+ expect(result).to include("Disabled")
+ expect(result).to include("Only team members")
+ expect(result).to include("Everyone with access")
+ end
+ end
+
+ context "when project is private" do
+ before { project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
+
+ it "shows only allowed options" do
+ helper.instance_variable_set(:@project, project)
+ result = helper.project_feature_access_select(:issues_access_level)
+ expect(result).to include("Disabled")
+ expect(result).to include("Only team members")
+ expect(result).not_to include("Everyone with access")
+ end
+ end
+
+ context "when project moves from public to private" do
+ before do
+ project.project_feature.update_attributes(issues_access_level: ProjectFeature::ENABLED)
+ project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it "shows the highest allowed level selected" do
+ helper.instance_variable_set(:@project, project)
+ result = helper.project_feature_access_select(:issues_access_level)
+
+ expect(result).to include("Disabled")
+ expect(result).to include("Only team members")
+ expect(result).not_to include("Everyone with access")
+ expect(result).to have_selector('option[selected]', text: "Only team members")
+ end
+ end
+ end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index b0bb991539b..c5b5aa8c445 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -6,6 +6,38 @@ describe SearchHelper do
str
end
+ describe 'parsing result' do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:results) { repository.search_files('feature', 'master') }
+ let(:search_result) { results.first }
+
+ subject { helper.parse_search_result(search_result) }
+
+ it "returns a valid OpenStruct object" do
+ is_expected.to be_an OpenStruct
+ expect(subject.filename).to eq('CHANGELOG')
+ expect(subject.basename).to eq('CHANGELOG')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(186)
+ expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
+ end
+
+ context "when filename has extension" do
+ let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
+
+ it { expect(subject.filename).to eq('CONTRIBUTE.md') }
+ it { expect(subject.basename).to eq('CONTRIBUTE') }
+ end
+
+ context "when file under directory" do
+ let(:search_result) { "master:a/b/c.md:5:a b c\n" }
+
+ it { expect(subject.filename).to eq('a/b/c.md') }
+ it { expect(subject.basename).to eq('a/b/c') }
+ end
+ end
+
describe 'search_autocomplete_source' do
context "with no current user" do
before do
@@ -32,6 +64,10 @@ describe SearchHelper do
expect(search_autocomplete_opts("adm").size).to eq(1)
end
+ it "does not allow regular expression in search term" do
+ expect(search_autocomplete_opts("(webhooks|api)").size).to eq(0)
+ end
+
it "includes the user's groups" do
create(:group).add_owner(user)
expect(search_autocomplete_opts("gro").size).to eq(1)
diff --git a/spec/helpers/sidekiq_helper_spec.rb b/spec/helpers/sidekiq_helper_spec.rb
new file mode 100644
index 00000000000..d60839b78ec
--- /dev/null
+++ b/spec/helpers/sidekiq_helper_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe SidekiqHelper do
+ describe 'parse_sidekiq_ps' do
+ it 'parses line with time' do
+ line = '55137 10,0 2,1 S+ 2:30pm sidekiq 4.1.4 gitlab [0 of 25 busy] '
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['55137', '10,0', '2,1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
+ end
+
+ it 'parses line with date' do
+ line = '55137 10,0 2,1 S+ Aug 4 sidekiq 4.1.4 gitlab [0 of 25 busy] '
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['55137', '10,0', '2,1', 'S+', 'Aug 4', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
+ end
+
+ it 'parses line with two digit date' do
+ line = '55137 10,0 2,1 S+ Aug 04 sidekiq 4.1.4 gitlab [0 of 25 busy] '
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['55137', '10,0', '2,1', 'S+', 'Aug 04', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
+ end
+
+ it 'parses line with dot as float separator' do
+ line = '55137 10.0 2.1 S+ 2:30pm sidekiq 4.1.4 gitlab [0 of 25 busy] '
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['55137', '10.0', '2.1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
+ end
+
+ it 'does fail gracefully on line not matching the format' do
+ line = '55137 10.0 2.1 S+ 2:30pm something'
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['?', '?', '?', '?', '?', '?'])
+ end
+ end
+end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index c1c12b57b53..019ce3b0702 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,13 +1,7 @@
/*= require awards_handler */
-
-
/*= require jquery */
-
-
/*= require jquery.cookie */
-
-
/*= require ./fixtures/emoji_menu */
(function() {
@@ -33,6 +27,7 @@
return setTimeout(function() {
assertFn();
return done();
+ // Maybe jasmine.clock here?
}, 333);
};
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 4c52ecd903d..13babb5bfdb 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -8,6 +8,7 @@
beforeEach(function() {
fixture.load('behaviors/quick_submit.html');
$('form').submit(function(e) {
+ // Prevent a form submit from moving us off the testing page
return e.preventDefault();
});
return this.spies = {
@@ -38,6 +39,8 @@
expect($('input[type=submit]')).toBeDisabled();
return 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', function() {
$('input.quick-submit-input').trigger(keydownEvent());
diff --git a/spec/javascripts/datetime_utility_spec.js.coffee b/spec/javascripts/datetime_utility_spec.js.coffee
deleted file mode 100644
index 8bd113e7d86..00000000000
--- a/spec/javascripts/datetime_utility_spec.js.coffee
+++ /dev/null
@@ -1,50 +0,0 @@
-#= require lib/utils/datetime_utility
-
-describe 'Date time utils', ->
- describe 'get day name', ->
- it 'should return Sunday', ->
- day = gl.utils.getDayName(new Date('07/17/2016'))
- expect(day).toBe('Sunday')
-
- it 'should return Monday', ->
- day = gl.utils.getDayName(new Date('07/18/2016'))
- expect(day).toBe('Monday')
-
- it 'should return Tuesday', ->
- day = gl.utils.getDayName(new Date('07/19/2016'))
- expect(day).toBe('Tuesday')
-
- it 'should return Wednesday', ->
- day = gl.utils.getDayName(new Date('07/20/2016'))
- expect(day).toBe('Wednesday')
-
- it 'should return Thursday', ->
- day = gl.utils.getDayName(new Date('07/21/2016'))
- expect(day).toBe('Thursday')
-
- it 'should return Friday', ->
- day = gl.utils.getDayName(new Date('07/22/2016'))
- expect(day).toBe('Friday')
-
- it 'should return Saturday', ->
- day = gl.utils.getDayName(new Date('07/23/2016'))
- expect(day).toBe('Saturday')
-
- describe 'get day difference', ->
- it 'should return 7', ->
- firstDay = new Date('07/01/2016')
- secondDay = new Date('07/08/2016')
- difference = gl.utils.getDayDifference(firstDay, secondDay)
- expect(difference).toBe(7)
-
- it 'should return 31', ->
- firstDay = new Date('07/01/2016')
- secondDay = new Date('08/01/2016')
- difference = gl.utils.getDayDifference(firstDay, secondDay)
- expect(difference).toBe(31)
-
- it 'should return 365', ->
- firstDay = new Date('07/02/2015')
- secondDay = new Date('07/01/2016')
- difference = gl.utils.getDayDifference(firstDay, secondDay)
- expect(difference).toBe(365) \ No newline at end of file
diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6
new file mode 100644
index 00000000000..a2d1b0a7732
--- /dev/null
+++ b/spec/javascripts/datetime_utility_spec.js.es6
@@ -0,0 +1,64 @@
+//= require lib/utils/datetime_utility
+(() => {
+ describe('Date time utils', () => {
+ describe('get day name', () => {
+ it('should return Sunday', () => {
+ const day = gl.utils.getDayName(new Date('07/17/2016'));
+ expect(day).toBe('Sunday');
+ });
+
+ it('should return Monday', () => {
+ const day = gl.utils.getDayName(new Date('07/18/2016'));
+ expect(day).toBe('Monday');
+ });
+
+ it('should return Tuesday', () => {
+ const day = gl.utils.getDayName(new Date('07/19/2016'));
+ expect(day).toBe('Tuesday');
+ });
+
+ it('should return Wednesday', () => {
+ const day = gl.utils.getDayName(new Date('07/20/2016'));
+ expect(day).toBe('Wednesday');
+ });
+
+ it('should return Thursday', () => {
+ const day = gl.utils.getDayName(new Date('07/21/2016'));
+ expect(day).toBe('Thursday');
+ });
+
+ it('should return Friday', () => {
+ const day = gl.utils.getDayName(new Date('07/22/2016'));
+ expect(day).toBe('Friday');
+ });
+
+ it('should return Saturday', () => {
+ const day = gl.utils.getDayName(new Date('07/23/2016'));
+ expect(day).toBe('Saturday');
+ });
+ });
+
+ describe('get day difference', () => {
+ it('should return 7', () => {
+ const firstDay = new Date('07/01/2016');
+ const secondDay = new Date('07/08/2016');
+ const difference = gl.utils.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(7);
+ });
+
+ it('should return 31', () => {
+ const firstDay = new Date('07/01/2016');
+ const secondDay = new Date('08/01/2016');
+ const difference = gl.utils.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(31);
+ });
+
+ it('should return 365', () => {
+ const firstDay = new Date('07/02/2015');
+ const secondDay = new Date('07/01/2016');
+ const difference = gl.utils.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(365);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/fixtures/comments.html.haml b/spec/javascripts/fixtures/comments.html.haml
new file mode 100644
index 00000000000..cc1f8f15c21
--- /dev/null
+++ b/spec/javascripts/fixtures/comments.html.haml
@@ -0,0 +1,21 @@
+.flash-container.timeline-content
+.timeline-icon.hidden-xs.hidden-sm
+ %a.author_link
+ %img
+.timeline-content.timeline-content-form
+ %form.new-note.js-quick-submit.common-note-form.gfm-form.js-main-target-form
+ .md-area
+ .md-header
+ .md-write-holder
+ .zen-backdrop.div-dropzone-wrapper
+ .div-dropzone-wrapper
+ .div-dropzone.dz-clickable
+ %textarea.note-textarea.js-note-text.js-gfm-input.js-autosize.markdown-area
+ .note-form-actions.clearfix
+ %input.btn.btn-nr.btn-create.append-right-10.comment-btn.js-comment-button{ type: 'submit' }
+ %a.btn.btn-nr.btn-reopen.btn-comment.js-note-target-reopen
+ Reopen issue
+ %a.btn.btn-nr.btn-close.btn-comment.js-note-target-close
+ Close issue
+ %a.btn.btn-cancel.js-note-discard
+ Discard draft \ No newline at end of file
diff --git a/spec/javascripts/fixtures/u2f/authenticate.html.haml b/spec/javascripts/fixtures/u2f/authenticate.html.haml
index 859e79a6c9e..779d6429a5f 100644
--- a/spec/javascripts/fixtures/u2f/authenticate.html.haml
+++ b/spec/javascripts/fixtures/u2f/authenticate.html.haml
@@ -1 +1 @@
-= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in" }
+= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in", params: {}, resource_name: "user" }
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index 82ee1954a59..d5401fbb0d1 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -7,7 +7,7 @@ describe("ContributorsGraph", function () {
expect(ContributorsGraph.prototype.x_domain).toEqual(20)
})
})
-
+
describe("#set_y_domain", function () {
it("sets the y_domain", function () {
ContributorsGraph.set_y_domain([{commits: 30}])
@@ -89,7 +89,7 @@ describe("ContributorsGraph", function () {
})
describe("ContributorsMasterGraph", function () {
-
+
// TODO: fix or remove
//describe("#process_dates", function () {
//it("gets and parses dates", function () {
@@ -103,7 +103,7 @@ describe("ContributorsMasterGraph", function () {
//expect(graph.get_dates).toHaveBeenCalledWith(data)
//expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get")
//})
- //})
+ //})
describe("#get_dates", function () {
it("plucks the date field from data collection", function () {
@@ -124,5 +124,5 @@ describe("ContributorsMasterGraph", function () {
})
})
-
+
})
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index dc6231ebb38..33690c7a5f3 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,7 +1,5 @@
/*= require lib/utils/text_utility */
-
-
/*= require issue */
(function() {
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
index 840c7b6d015..1ad6f612210 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js.es6
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -48,9 +48,9 @@
setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10);
- $('.dropdow-content a').each((i, $link) => {
- if (i < 5) {
- $link.get(0).click();
+ $('.dropdown-content a').each(function (i) {
+ if (i < saveLabelCount) {
+ $(this).get(0).click();
}
});
@@ -70,9 +70,9 @@
setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10);
- $('.dropdow-content a').each((i, $link) => {
- if (i < 5) {
- $link.get(0).click();
+ $('.dropdown-content a').each(function (i) {
+ if (i < saveLabelCount) {
+ $(this).get(0).click();
}
});
@@ -86,4 +86,3 @@
});
});
})();
-
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index 25d3f5b6c04..f09596bd36d 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,7 +1,5 @@
/*= require jquery-ui/autocomplete */
-
-
/*= require new_branch_form */
(function() {
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 14dc6bfdfde..a588f403dd5 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,8 +1,7 @@
-
/*= require notes */
-
-
+/*= require autosize */
/*= require gl_form */
+/*= require lib/utils/text_utility */
(function() {
window.gon || (window.gon = {});
@@ -12,29 +11,63 @@
};
describe('Notes', function() {
- return describe('task lists', function() {
+ describe('task lists', function() {
fixture.preload('issue_note.html');
+
beforeEach(function() {
fixture.load('issue_note.html');
$('form').on('submit', function(e) {
- return e.preventDefault();
+ e.preventDefault();
});
- return this.notes = new Notes();
+ this.notes = new Notes();
});
+
it('modifies the Markdown field', function() {
$('input[type=checkbox]').attr('checked', true).trigger('change');
- return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+ expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
- return it('submits the form on tasklist:changed', function() {
- var submitted;
- submitted = false;
+
+ it('submits the form on tasklist:changed', function() {
+ var submitted = false;
$('form').on('submit', function(e) {
submitted = true;
- return e.preventDefault();
+ e.preventDefault();
});
+
$('.js-task-list-field').trigger('tasklist:changed');
- return expect(submitted).toBe(true);
+ expect(submitted).toBe(true);
+ });
+ });
+
+ describe('comments', function() {
+ var commentsTemplate = 'comments.html';
+ var textarea = '.js-note-text';
+ fixture.preload(commentsTemplate);
+
+ beforeEach(function() {
+ fixture.load(commentsTemplate);
+ this.notes = new Notes();
+
+ this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update');
+ spyOn(this.notes, 'renderNote').and.stub();
+
+ $(textarea).data('autosave', {
+ reset: function() {}
+ });
+
+ $('form').on('submit', function(e) {
+ e.preventDefault();
+ $('.js-main-target-form').trigger('ajax:success');
+ });
});
+
+ it('autosizes after comment submission', function() {
+ $(textarea).text('This is an example comment note');
+ expect(this.autoSizeSpy).not.toHaveBeenTriggered();
+
+ $('.js-comment-button').click();
+ expect(this.autoSizeSpy).toHaveBeenTriggered();
+ })
});
});
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index ffe49828492..51eb12b41d4 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -1,22 +1,10 @@
/*= require bootstrap */
-
-
/*= require select2 */
-
-
/*= require lib/utils/type_utility */
-
-
/*= require gl_dropdown */
-
-
/*= require api */
-
-
/*= require project_select */
-
-
/*= require project */
(function() {
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 38b3b2653ec..c937a4706f7 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,10 +1,6 @@
/*= require right_sidebar */
-
-
/*= require jquery */
-
-
/*= require jquery.cookie */
(function() {
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 324f5152780..00d9fc1302a 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,19 +1,9 @@
/*= require gl_dropdown */
-
-
/*= require search_autocomplete */
-
-
/*= require jquery */
-
-
/*= require lib/utils/common_utils */
-
-
/*= require lib/utils/type_utility */
-
-
/*= require fuzzaldrin-plus */
(function() {
@@ -43,6 +33,8 @@
groupName = 'Gitlab Org';
+ // Add required attributes to body before starting the test.
+ // section would be dashboard|group|project
addBodyAttributes = function(section) {
var $body;
if (section == null) {
@@ -64,6 +56,7 @@
}
};
+ // Mock `gl` object in window for dashboard specific page. App code will need it.
mockDashboardOptions = function() {
window.gl || (window.gl = {});
return window.gl.dashboardOptions = {
@@ -72,6 +65,7 @@
};
};
+ // Mock `gl` object in window for project specific page. App code will need it.
mockProjectOptions = function() {
window.gl || (window.gl = {});
return window.gl.projectOptions = {
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 7b6b55fe545..04ccf246052 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -10,6 +10,7 @@
});
return describe('#replyWithSelectedText', function() {
var stubSelection;
+ // Stub window.getSelection to return the provided String.
stubSelection = function(text) {
return window.getSelection = function() {
return text;
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
index 7d91ed0f855..8801c297887 100644
--- a/spec/javascripts/spec_helper.js
+++ b/spec/javascripts/spec_helper.js
@@ -1,21 +1,41 @@
-
+// PhantomJS (Teaspoons default driver) doesn't have support for
+// Function.prototype.bind, which has caused confusion. Use this polyfill to
+// avoid the confusion.
/*= require support/bind-poly */
-
+// You can require your own javascript files here. By default this will include
+// everything in application, however you may get better load performance if you
+// require the specific files that are being used in the spec that tests them.
/*= require jquery */
-
-
/*= require jquery.turbolinks */
-
-
/*= require bootstrap */
-
-
/*= require underscore */
-
+// Teaspoon includes some support files, but you can use anything from your own
+// support path too.
+// require support/jasmine-jquery-1.7.0
+// require support/jasmine-jquery-2.0.0
/*= require support/jasmine-jquery-2.1.0 */
+// require support/sinon
+// require support/your-support-file
+// Deferring execution
+// If you're using CommonJS, RequireJS or some other asynchronous library you can
+// defer execution. Call Teaspoon.execute() after everything has been loaded.
+// Simple example of a timeout:
+// Teaspoon.defer = true
+// setTimeout(Teaspoon.execute, 1000)
+// Matching files
+// By default Teaspoon will look for files that match
+// _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path
+// and it'll be included in the default suite automatically. If you want to
+// customize suites, check out the configuration in teaspoon_env.rb
+// Manifest
+// If you'd rather require your spec files manually (to control order for
+// instance) you can disable the suite matcher in the configuration and use this
+// file as a manifest.
+// For more information: http://github.com/modeset/teaspoon
+
(function() {
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index e008ce956ad..7ce3884f844 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -1,16 +1,8 @@
/*= require u2f/authenticate */
-
-
/*= require u2f/util */
-
-
/*= require u2f/error */
-
-
/*= require u2f */
-
-
/*= require ./mock_u2f_device */
(function() {
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 21c5266c60e..01d6b7a8961 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -1,16 +1,8 @@
/*= require u2f/register */
-
-
/*= require u2f/util */
-
-
/*= require u2f/error */
-
-
/*= require u2f */
-
-
/*= require ./mock_u2f_device */
(function() {
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index 3d680ec8ea3..0c1266800d7 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -14,8 +14,10 @@
return true;
}
};
+ // Stub Dropzone.forElement(...).enable()
});
this.zen = new ZenMode();
+ // Set this manually because we can't actually scroll the window
return this.zen.scroll_position = 456;
});
describe('on enter', function() {
@@ -60,7 +62,7 @@
return $('a.js-zen-enter').click();
};
- exitZen = function() {
+ exitZen = function() { // Ohmmmmmmm
return $('a.js-zen-leave').click();
};
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 51c89ac4889..ac9bde6baf1 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -127,6 +127,13 @@ describe Banzai::Pipeline::WikiPipeline do
expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"")
end
+
+ it 'rewrites links with anchor' do
+ markdown = '[Link to Header](start-page#title)'
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/start-page#title\"")
+ end
end
describe "when creating root links" do
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index be51d942af7..6dedd25e9d3 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -754,6 +754,20 @@ module Ci
it 'does return production' do
expect(builds.size).to eq(1)
expect(builds.first[:environment]).to eq(environment)
+ expect(builds.first[:options]).to include(environment: { name: environment })
+ end
+ end
+
+ context 'when hash is specified' do
+ let(:environment) do
+ { name: 'production',
+ url: 'http://production.gitlab.com' }
+ end
+
+ it 'does return production and URL' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to eq(environment[:name])
+ expect(builds.first[:options]).to include(environment: environment)
end
end
@@ -770,15 +784,16 @@ module Ci
let(:environment) { 1 }
it 'raises error' do
- expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}")
+ expect { builds }.to raise_error(
+ 'jobs:deploy_to_production:environment config should be a hash or a string')
end
end
context 'is not a valid string' do
- let(:environment) { 'production staging' }
+ let(:environment) { 'production:staging' }
it 'raises error' do
- expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}")
+ expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
end
end
end
@@ -1250,5 +1265,40 @@ EOT
end
end
end
+
+ describe "#validation_message" do
+ context "when the YAML could not be parsed" do
+ it "returns an error about invalid configutaion" do
+ content = YAML.dump("invalid: yaml: test")
+
+ expect(GitlabCiYamlProcessor.validation_message(content))
+ .to eq "Invalid configuration format"
+ end
+ end
+
+ context "when the tags parameter is invalid" do
+ it "returns an error about invalid tags" do
+ content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
+
+ expect(GitlabCiYamlProcessor.validation_message(content))
+ .to eq "jobs:rspec tags should be an array of strings"
+ end
+ end
+
+ context "when YAML content is empty" do
+ it "returns an error about missing content" do
+ expect(GitlabCiYamlProcessor.validation_message(''))
+ .to eq "Please provide content of .gitlab-ci.yml"
+ end
+ end
+
+ context "when the YAML is valid" do
+ it "does not return any errors" do
+ content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+
+ expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/lib/ci/mask_secret_spec.rb b/spec/lib/ci/mask_secret_spec.rb
new file mode 100644
index 00000000000..3101bed20fb
--- /dev/null
+++ b/spec/lib/ci/mask_secret_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Ci::MaskSecret, lib: true do
+ subject { described_class }
+
+ describe '#mask' do
+ it 'masks exact number of characters' do
+ expect(mask('token', 'oke')).to eq('txxxn')
+ end
+
+ it 'masks multiple occurrences' do
+ expect(mask('token token token', 'oke')).to eq('txxxn txxxn txxxn')
+ end
+
+ it 'does not mask if not found' do
+ expect(mask('token', 'not')).to eq('token')
+ end
+
+ it 'does support null token' do
+ expect(mask('token', nil)).to eq('token')
+ end
+
+ def mask(value, token)
+ subject.mask!(value.dup, token)
+ end
+ end
+end
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
new file mode 100644
index 00000000000..90bc7dad379
--- /dev/null
+++ b/spec/lib/expand_variables_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe ExpandVariables do
+ describe '#expand' do
+ subject { described_class.expand(value, variables) }
+
+ tests = [
+ { value: 'key',
+ result: 'key',
+ variables: []
+ },
+ { value: 'key$variable',
+ result: 'key',
+ variables: []
+ },
+ { value: 'key$variable',
+ result: 'keyvalue',
+ variables: [
+ { key: 'variable', value: 'value' }
+ ]
+ },
+ { value: 'key${variable}',
+ result: 'keyvalue',
+ variables: [
+ { key: 'variable', value: 'value' }
+ ]
+ },
+ { value: 'key$variable$variable2',
+ result: 'keyvalueresult',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' },
+ ]
+ },
+ { value: 'key${variable}${variable2}',
+ result: 'keyvalueresult',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' }
+ ]
+ },
+ { value: 'key$variable2$variable',
+ result: 'keyresultvalue',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' },
+ ]
+ },
+ { value: 'key${variable2}${variable}',
+ result: 'keyresultvalue',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' }
+ ]
+ },
+ { value: 'review/$CI_BUILD_REF_NAME',
+ result: 'review/feature/add-review-apps',
+ variables: [
+ { key: 'CI_BUILD_REF_NAME', value: 'feature/add-review-apps' }
+ ]
+ },
+ ]
+
+ tests.each do |test|
+ context "#{test[:value]} resolves to #{test[:result]}" do
+ let(:value) { test[:value] }
+ let(:variables) { test[:variables] }
+
+ it { is_expected.to eq(test[:result]) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 7c23e02d05a..c9d64e99f88 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -4,15 +4,53 @@ describe Gitlab::Auth, lib: true do
let(:gl_auth) { described_class }
describe 'find_for_git_client' do
- it 'recognizes CI' do
- token = '123'
+ context 'build token' do
+ subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: project, ip: 'ip') }
+
+ context 'for running build' do
+ let!(:build) { create(:ci_build, :running) }
+ let(:project) { build.project }
+
+ before do
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'gitlab-ci-token')
+ end
+
+ it 'recognises user-less build' do
+ expect(subject).to eq(Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities))
+ end
+
+ it 'recognises user token' do
+ build.update(user: create(:user))
+
+ expect(subject).to eq(Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities))
+ end
+ end
+
+ (HasStatus::AVAILABLE_STATUSES - ['running']).each do |build_status|
+ context "for #{build_status} build" do
+ let!(:build) { create(:ci_build, status: build_status) }
+ let(:project) { build.project }
+
+ before do
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'gitlab-ci-token')
+ end
+
+ it 'denies authentication' do
+ expect(subject).to eq(Gitlab::Auth::Result.new)
+ end
+ end
+ end
+ end
+
+ it 'recognizes other ci services' do
project = create(:empty_project)
- project.update_attributes(runners_token: token)
+ project.create_drone_ci_service(active: true)
+ project.drone_ci_service.update(token: 'token')
ip = 'ip'
- expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token')
- expect(gl_auth.find_for_git_client('gitlab-ci-token', token, project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, :ci))
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token')
+ expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
end
it 'recognizes master passwords' do
@@ -20,7 +58,25 @@ describe Gitlab::Auth, lib: true do
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
- expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :gitlab_or_ldap))
+ expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ end
+
+ it 'recognizes user lfs tokens' do
+ user = create(:user)
+ ip = 'ip'
+ token = Gitlab::LfsToken.new(user).token
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
+ expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
+ end
+
+ it 'recognizes deploy key lfs tokens' do
+ key = create(:deploy_key)
+ ip = 'ip'
+ token = Gitlab::LfsToken.new(key).token
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}")
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
end
it 'recognizes OAuth tokens' do
@@ -30,7 +86,7 @@ describe Gitlab::Auth, lib: true do
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2')
- expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :oauth))
+ expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
end
it 'returns double nil for invalid credentials' do
@@ -92,4 +148,30 @@ describe Gitlab::Auth, lib: true do
end
end
end
+
+ private
+
+ def build_authentication_abilities
+ [
+ :read_project,
+ :build_download_code,
+ :build_read_container_image,
+ :build_create_container_image
+ ]
+ end
+
+ def read_authentication_abilities
+ [
+ :read_project,
+ :download_code,
+ :read_container_image
+ ]
+ end
+
+ def full_authentication_abilities
+ read_authentication_abilities + [
+ :push_code,
+ :create_container_image
+ ]
+ end
end
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb
index 6e5ba211382..07407f212aa 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/backend/shell_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+require 'stringio'
describe Gitlab::Shell, lib: true do
let(:project) { double('Project', id: 7, path: 'diaspora') }
@@ -44,15 +45,38 @@ describe Gitlab::Shell, lib: true do
end
end
+ describe '#add_key' do
+ it 'removes trailing garbage' do
+ allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
+ expect(Gitlab::Utils).to receive(:system_silent).with(
+ [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar']
+ )
+
+ gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+ end
+ end
+
describe Gitlab::Shell::KeyAdder, lib: true do
describe '#add_key' do
- it 'normalizes space characters in the key' do
- io = spy
+ it 'removes trailing garbage' do
+ io = spy(:io)
adder = described_class.new(io)
- adder.add_key('key-42', "sha-rsa foo\tbar\tbaz")
+ adder.add_key('key-42', "ssh-rsa foo bar\tbaz")
+
+ expect(io).to have_received(:puts).with("key-42\tssh-rsa foo")
+ end
+
+ it 'raises an exception if the key contains a tab' do
+ expect do
+ described_class.new(StringIO.new).add_key('key-42', "ssh-rsa\tfoobar")
+ end.to raise_error(Gitlab::Shell::Error)
+ end
- expect(io).to have_received(:puts).with("key-42\tsha-rsa foo bar baz")
+ it 'raises an exception if the key contains a newline' do
+ expect do
+ described_class.new(StringIO.new).add_key('key-42', "ssh-rsa foobar\nssh-rsa pawned")
+ end.to raise_error(Gitlab::Shell::Error)
end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/cache_spec.rb b/spec/lib/gitlab/ci/config/node/cache_spec.rb
index 50f619ce26e..e251210949c 100644
--- a/spec/lib/gitlab/ci/config/node/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/cache_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Cache do
let(:entry) { described_class.new(config) }
describe 'validations' do
- before { entry.process! }
+ before { entry.compose! }
context 'when entry config value is correct' do
let(:config) do
diff --git a/spec/lib/gitlab/ci/config/node/environment_spec.rb b/spec/lib/gitlab/ci/config/node/environment_spec.rb
new file mode 100644
index 00000000000..df453223da7
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/environment_spec.rb
@@ -0,0 +1,155 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Environment do
+ let(:entry) { described_class.new(config) }
+
+ before { entry.compose! }
+
+ context 'when configuration is a string' do
+ let(:config) { 'production' }
+
+ describe '#string?' do
+ it 'is string configuration' do
+ expect(entry).to be_string
+ end
+ end
+
+ describe '#hash?' do
+ it 'is not hash configuration' do
+ expect(entry).not_to be_hash
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid hash' do
+ expect(entry.value).to eq(name: 'production')
+ end
+ end
+
+ describe '#name' do
+ it 'returns environment name' do
+ expect(entry.name).to eq 'production'
+ end
+ end
+
+ describe '#url' do
+ it 'returns environment url' do
+ expect(entry.url).to be_nil
+ end
+ end
+ end
+
+ context 'when configuration is a hash' do
+ let(:config) do
+ { name: 'development', url: 'https://example.gitlab.com' }
+ end
+
+ describe '#string?' do
+ it 'is not string configuration' do
+ expect(entry).not_to be_string
+ end
+ end
+
+ describe '#hash?' do
+ it 'is hash configuration' do
+ expect(entry).to be_hash
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid hash' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#name' do
+ it 'returns environment name' do
+ expect(entry.name).to eq 'development'
+ end
+ end
+
+ describe '#url' do
+ it 'returns environment url' do
+ expect(entry.url).to eq 'https://example.gitlab.com'
+ end
+ end
+ end
+
+ context 'when variables are used for environment' do
+ let(:config) do
+ { name: 'review/$CI_BUILD_REF_NAME',
+ url: 'https://$CI_BUILD_REF_NAME.review.gitlab.com' }
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when configuration is invalid' do
+ context 'when configuration is an array' do
+ let(:config) { ['env'] }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about invalid type' do
+ expect(entry.errors)
+ .to include 'environment config should be a hash or a string'
+ end
+ end
+ end
+
+ context 'when environment name is not present' do
+ let(:config) { { url: 'https://example.gitlab.com' } }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors?' do
+ it 'contains error about missing environment name' do
+ expect(entry.errors)
+ .to include "environment name can't be blank"
+ end
+ end
+ end
+
+ context 'when invalid URL is used' do
+ let(:config) { { name: 'test', url: 'invalid-example.gitlab.com' } }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors?' do
+ it 'contains error about invalid URL' do
+ expect(entry.errors)
+ .to include "environment url must be a valid url"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb
index d26185ba585..a699089c563 100644
--- a/spec/lib/gitlab/ci/config/node/factory_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb
@@ -65,7 +65,8 @@ describe Gitlab::Ci::Config::Node::Factory do
.value(nil)
.create!
- expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
+ expect(entry)
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified
end
end
diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb
index 2f87d270b36..12232ff7e2f 100644
--- a/spec/lib/gitlab/ci/config/node/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/global_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Config::Node::Global do
end
context 'when hash is valid' do
- context 'when all entries defined' do
+ context 'when some entries defined' do
let(:hash) do
{ before_script: ['ls', 'pwd'],
image: 'ruby:2.2',
@@ -24,11 +24,11 @@ describe Gitlab::Ci::Config::Node::Global do
stages: ['build', 'pages'],
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
- spinach: { script: 'spinach' } }
+ spinach: { before_script: [], variables: {}, script: 'spinach' } }
end
- describe '#process!' do
- before { global.process! }
+ describe '#compose!' do
+ before { global.compose! }
it 'creates nodes hash' do
expect(global.descendants).to be_an Array
@@ -59,7 +59,7 @@ describe Gitlab::Ci::Config::Node::Global do
end
end
- context 'when not processed' do
+ context 'when not composed' do
describe '#before_script' do
it 'returns nil' do
expect(global.before_script).to be nil
@@ -73,8 +73,14 @@ describe Gitlab::Ci::Config::Node::Global do
end
end
- context 'when processed' do
- before { global.process! }
+ context 'when composed' do
+ before { global.compose! }
+
+ describe '#errors' do
+ it 'has no errors' do
+ expect(global.errors).to be_empty
+ end
+ end
describe '#before_script' do
it 'returns correct script' do
@@ -137,10 +143,24 @@ describe Gitlab::Ci::Config::Node::Global do
expect(global.jobs).to eq(
rspec: { name: :rspec,
script: %w[rspec ls],
- stage: 'test' },
+ before_script: ['ls', 'pwd'],
+ commands: "ls\npwd\nrspec\nls",
+ image: 'ruby:2.2',
+ services: ['postgres:9.1', 'mysql:5.5'],
+ stage: 'test',
+ cache: { key: 'k', untracked: true, paths: ['public/'] },
+ variables: { VAR: 'value' },
+ after_script: ['make clean'] },
spinach: { name: :spinach,
+ before_script: [],
script: %w[spinach],
- stage: 'test' }
+ commands: 'spinach',
+ image: 'ruby:2.2',
+ services: ['postgres:9.1', 'mysql:5.5'],
+ stage: 'test',
+ cache: { key: 'k', untracked: true, paths: ['public/'] },
+ variables: {},
+ after_script: ['make clean'] },
)
end
end
@@ -148,17 +168,20 @@ describe Gitlab::Ci::Config::Node::Global do
end
context 'when most of entires not defined' do
- let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } }
- before { global.process! }
+ before { global.compose! }
+
+ let(:hash) do
+ { cache: { key: 'a' }, rspec: { script: %w[ls] } }
+ end
describe '#nodes' do
it 'instantizes all nodes' do
expect(global.descendants.count).to eq 8
end
- it 'contains undefined nodes' do
+ it 'contains unspecified nodes' do
expect(global.descendants.first)
- .to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified
end
end
@@ -188,8 +211,11 @@ describe Gitlab::Ci::Config::Node::Global do
# details.
#
context 'when entires specified but not defined' do
- let(:hash) { { variables: nil, rspec: { script: 'rspec' } } }
- before { global.process! }
+ before { global.compose! }
+
+ let(:hash) do
+ { variables: nil, rspec: { script: 'rspec' } }
+ end
describe '#variables' do
it 'undefined entry returns a default value' do
@@ -200,7 +226,7 @@ describe Gitlab::Ci::Config::Node::Global do
end
context 'when hash is not valid' do
- before { global.process! }
+ before { global.compose! }
let(:hash) do
{ before_script: 'ls' }
@@ -247,4 +273,27 @@ describe Gitlab::Ci::Config::Node::Global do
expect(global.specified?).to be true
end
end
+
+ describe '#[]' do
+ before { global.compose! }
+
+ let(:hash) do
+ { cache: { key: 'a' }, rspec: { script: 'ls' } }
+ end
+
+ context 'when node exists' do
+ it 'returns correct entry' do
+ expect(global[:cache])
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Cache
+ expect(global[:jobs][:rspec][:script].value).to eq ['ls']
+ end
+ end
+
+ context 'when node does not exist' do
+ it 'always return unspecified node' do
+ expect(global[:some][:unknown][:node])
+ .not_to be_specified
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb
index 1484fb60dd8..91f676dae03 100644
--- a/spec/lib/gitlab/ci/config/node/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/job_spec.rb
@@ -3,9 +3,9 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Node::Job do
let(:entry) { described_class.new(config, name: :rspec) }
- before { entry.process! }
-
describe 'validations' do
+ before { entry.compose! }
+
context 'when entry config value is correct' do
let(:config) { { script: 'rspec' } }
@@ -59,28 +59,82 @@ describe Gitlab::Ci::Config::Node::Job do
end
end
- describe '#value' do
- context 'when entry is correct' do
+ describe '#relevant?' do
+ it 'is a relevant entry' do
+ expect(entry).to be_relevant
+ end
+ end
+
+ describe '#compose!' do
+ let(:unspecified) { double('unspecified', 'specified?' => false) }
+
+ let(:specified) do
+ double('specified', 'specified?' => true, value: 'specified')
+ end
+
+ let(:deps) { double('deps', '[]' => unspecified) }
+
+ context 'when job config overrides global config' do
+ before { entry.compose!(deps) }
+
let(:config) do
- { before_script: %w[ls pwd],
- script: 'rspec',
- after_script: %w[cleanup] }
+ { image: 'some_image', cache: { key: 'test' } }
+ end
+
+ it 'overrides global config' do
+ expect(entry[:image].value).to eq 'some_image'
+ expect(entry[:cache].value).to eq(key: 'test')
+ end
+ end
+
+ context 'when job config does not override global config' do
+ before do
+ allow(deps).to receive('[]').with(:image).and_return(specified)
+ entry.compose!(deps)
end
- it 'returns correct value' do
- expect(entry.value)
- .to eq(name: :rspec,
- before_script: %w[ls pwd],
- script: %w[rspec],
- stage: 'test',
- after_script: %w[cleanup])
+ let(:config) { { script: 'ls', cache: { key: 'test' } } }
+
+ it 'uses config from global entry' do
+ expect(entry[:image].value).to eq 'specified'
+ expect(entry[:cache].value).to eq(key: 'test')
end
end
end
- describe '#relevant?' do
- it 'is a relevant entry' do
- expect(entry).to be_relevant
+ context 'when composed' do
+ before { entry.compose! }
+
+ describe '#value' do
+ before { entry.compose! }
+
+ context 'when entry is correct' do
+ let(:config) do
+ { before_script: %w[ls pwd],
+ script: 'rspec',
+ after_script: %w[cleanup] }
+ end
+
+ it 'returns correct value' do
+ expect(entry.value)
+ .to eq(name: :rspec,
+ before_script: %w[ls pwd],
+ script: %w[rspec],
+ commands: "ls\npwd\nrspec",
+ stage: 'test',
+ after_script: %w[cleanup])
+ end
+ end
+ end
+
+ describe '#commands' do
+ let(:config) do
+ { before_script: %w[ls pwd], script: 'rspec' }
+ end
+
+ it 'returns a string of commands concatenated with new line character' do
+ expect(entry.commands).to eq "ls\npwd\nrspec"
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
index ae2c88aac37..929809339ef 100644
--- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Jobs do
let(:entry) { described_class.new(config) }
describe 'validations' do
- before { entry.process! }
+ before { entry.compose! }
context 'when entry config value is correct' do
let(:config) { { rspec: { script: 'rspec' } } }
@@ -47,8 +47,8 @@ describe Gitlab::Ci::Config::Node::Jobs do
end
end
- context 'when valid job entries processed' do
- before { entry.process! }
+ context 'when valid job entries composed' do
+ before { entry.compose! }
let(:config) do
{ rspec: { script: 'rspec' },
@@ -61,9 +61,11 @@ describe Gitlab::Ci::Config::Node::Jobs do
expect(entry.value).to eq(
rspec: { name: :rspec,
script: %w[rspec],
+ commands: 'rspec',
stage: 'test' },
spinach: { name: :spinach,
script: %w[spinach],
+ commands: 'spinach',
stage: 'test' })
end
end
diff --git a/spec/lib/gitlab/ci/config/node/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb
deleted file mode 100644
index 1ab5478dcfa..00000000000
--- a/spec/lib/gitlab/ci/config/node/null_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Null do
- let(:null) { described_class.new(nil) }
-
- describe '#leaf?' do
- it 'is leaf node' do
- expect(null).to be_leaf
- end
- end
-
- describe '#valid?' do
- it 'is always valid' do
- expect(null).to be_valid
- end
- end
-
- describe '#errors' do
- it 'is does not contain errors' do
- expect(null.errors).to be_empty
- end
- end
-
- describe '#value' do
- it 'returns nil' do
- expect(null.value).to eq nil
- end
- end
-
- describe '#relevant?' do
- it 'is not relevant' do
- expect(null.relevant?).to eq false
- end
- end
-
- describe '#specified?' do
- it 'is not defined' do
- expect(null.specified?).to eq false
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb
index ee7395362a9..219a7e981d3 100644
--- a/spec/lib/gitlab/ci/config/node/script_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/script_spec.rb
@@ -3,9 +3,7 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Node::Script do
let(:entry) { described_class.new(config) }
- describe '#process!' do
- before { entry.process! }
-
+ describe 'validations' do
context 'when entry config value is correct' do
let(:config) { ['ls', 'pwd'] }
diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/node/undefined_spec.rb
index 2d43e1c1a9d..6bde8602963 100644
--- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/undefined_spec.rb
@@ -1,32 +1,41 @@
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Undefined do
- let(:undefined) { described_class.new(entry) }
- let(:entry) { spy('Entry') }
+ let(:entry) { described_class.new }
+
+ describe '#leaf?' do
+ it 'is leaf node' do
+ expect(entry).to be_leaf
+ end
+ end
describe '#valid?' do
- it 'delegates method to entry' do
- expect(undefined.valid).to eq entry
+ it 'is always valid' do
+ expect(entry).to be_valid
end
end
describe '#errors' do
- it 'delegates method to entry' do
- expect(undefined.errors).to eq entry
+ it 'is does not contain errors' do
+ expect(entry.errors).to be_empty
end
end
describe '#value' do
- it 'delegates method to entry' do
- expect(undefined.value).to eq entry
+ it 'returns nil' do
+ expect(entry.value).to eq nil
end
end
- describe '#specified?' do
- it 'is always false' do
- allow(entry).to receive(:specified?).and_return(true)
+ describe '#relevant?' do
+ it 'is not relevant' do
+ expect(entry.relevant?).to eq false
+ end
+ end
- expect(undefined.specified?).to be false
+ describe '#specified?' do
+ it 'is not defined' do
+ expect(entry.specified?).to eq false
end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/unspecified_spec.rb b/spec/lib/gitlab/ci/config/node/unspecified_spec.rb
new file mode 100644
index 00000000000..ba3ceef24ce
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/unspecified_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Unspecified do
+ let(:unspecified) { described_class.new(entry) }
+ let(:entry) { spy('Entry') }
+
+ describe '#valid?' do
+ it 'delegates method to entry' do
+ expect(unspecified.valid?).to eq entry
+ end
+ end
+
+ describe '#errors' do
+ it 'delegates method to entry' do
+ expect(unspecified.errors).to eq entry
+ end
+ end
+
+ describe '#value' do
+ it 'delegates method to entry' do
+ expect(unspecified.value).to eq entry
+ end
+ end
+
+ describe '#specified?' do
+ it 'is always false' do
+ allow(entry).to receive(:specified?).and_return(true)
+
+ expect(unspecified.specified?).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline_duration_spec.rb b/spec/lib/gitlab/ci/pipeline_duration_spec.rb
new file mode 100644
index 00000000000..b26728a843c
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline_duration_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::PipelineDuration do
+ let(:calculated_duration) { calculate(data) }
+
+ shared_examples 'calculating duration' do
+ it do
+ expect(calculated_duration).to eq(duration)
+ end
+ end
+
+ context 'test sample A' do
+ let(:data) do
+ [[0, 1],
+ [1, 2],
+ [3, 4],
+ [5, 6]]
+ end
+
+ let(:duration) { 4 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample B' do
+ let(:data) do
+ [[0, 1],
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [0, 4]]
+ end
+
+ let(:duration) { 4 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample C' do
+ let(:data) do
+ [[0, 4],
+ [2, 6],
+ [5, 7],
+ [8, 9]]
+ end
+
+ let(:duration) { 8 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample D' do
+ let(:data) do
+ [[0, 1],
+ [2, 3],
+ [4, 5],
+ [6, 7]]
+ end
+
+ let(:duration) { 4 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample E' do
+ let(:data) do
+ [[0, 1],
+ [3, 9],
+ [3, 4],
+ [3, 5],
+ [3, 8],
+ [4, 5],
+ [4, 7],
+ [5, 8]]
+ end
+
+ let(:duration) { 7 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample F' do
+ let(:data) do
+ [[1, 3],
+ [2, 4],
+ [2, 4],
+ [2, 4],
+ [5, 8]]
+ end
+
+ let(:duration) { 6 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample G' do
+ let(:data) do
+ [[1, 3],
+ [2, 4],
+ [6, 7]]
+ end
+
+ let(:duration) { 4 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ def calculate(data)
+ periods = data.shuffle.map do |(first, last)|
+ Gitlab::Ci::PipelineDuration::Period.new(first, last)
+ end
+
+ Gitlab::Ci::PipelineDuration.from_periods(periods.sort_by(&:first))
+ end
+end
diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb
index a1d2ca1e272..16eb3766356 100644
--- a/spec/lib/gitlab/conflict/parser_spec.rb
+++ b/spec/lib/gitlab/conflict/parser_spec.rb
@@ -179,8 +179,8 @@ CONFLICT
to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
end
- it 'raises UnmergeableFile when the file is over 100 KB' do
- expect { parse_text('a' * 102401) }.
+ it 'raises UnmergeableFile when the file is over 200 KB' do
+ expect { parse_text('a' * 204801) }.
to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 4ec3f19e03f..7fd25b9e5bf 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -91,63 +91,80 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
describe '#add_column_with_default' do
context 'outside of a transaction' do
- before do
- expect(model).to receive(:transaction_open?).and_return(false)
+ context 'when a column limit is not set' do
+ before do
+ expect(model).to receive(:transaction_open?).and_return(false)
- expect(model).to receive(:transaction).and_yield
+ expect(model).to receive(:transaction).and_yield
- expect(model).to receive(:add_column).
- with(:projects, :foo, :integer, default: nil)
+ expect(model).to receive(:add_column).
+ with(:projects, :foo, :integer, default: nil)
- expect(model).to receive(:change_column_default).
- with(:projects, :foo, 10)
- end
+ expect(model).to receive(:change_column_default).
+ with(:projects, :foo, 10)
+ end
- it 'adds the column while allowing NULL values' do
- expect(model).to receive(:update_column_in_batches).
- with(:projects, :foo, 10)
+ it 'adds the column while allowing NULL values' do
+ expect(model).to receive(:update_column_in_batches).
+ with(:projects, :foo, 10)
- expect(model).not_to receive(:change_column_null)
+ expect(model).not_to receive(:change_column_null)
- model.add_column_with_default(:projects, :foo, :integer,
- default: 10,
- allow_null: true)
- end
+ model.add_column_with_default(:projects, :foo, :integer,
+ default: 10,
+ allow_null: true)
+ end
- it 'adds the column while not allowing NULL values' do
- expect(model).to receive(:update_column_in_batches).
- with(:projects, :foo, 10)
+ it 'adds the column while not allowing NULL values' do
+ expect(model).to receive(:update_column_in_batches).
+ with(:projects, :foo, 10)
- expect(model).to receive(:change_column_null).
- with(:projects, :foo, false)
+ expect(model).to receive(:change_column_null).
+ with(:projects, :foo, false)
- model.add_column_with_default(:projects, :foo, :integer, default: 10)
- end
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end
- it 'removes the added column whenever updating the rows fails' do
- expect(model).to receive(:update_column_in_batches).
- with(:projects, :foo, 10).
- and_raise(RuntimeError)
+ it 'removes the added column whenever updating the rows fails' do
+ expect(model).to receive(:update_column_in_batches).
+ with(:projects, :foo, 10).
+ and_raise(RuntimeError)
- expect(model).to receive(:remove_column).
- with(:projects, :foo)
+ expect(model).to receive(:remove_column).
+ with(:projects, :foo)
- expect do
- model.add_column_with_default(:projects, :foo, :integer, default: 10)
- end.to raise_error(RuntimeError)
+ expect do
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end.to raise_error(RuntimeError)
+ end
+
+ it 'removes the added column whenever changing a column NULL constraint fails' do
+ expect(model).to receive(:change_column_null).
+ with(:projects, :foo, false).
+ and_raise(RuntimeError)
+
+ expect(model).to receive(:remove_column).
+ with(:projects, :foo)
+
+ expect do
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end.to raise_error(RuntimeError)
+ end
end
- it 'removes the added column whenever changing a column NULL constraint fails' do
- expect(model).to receive(:change_column_null).
- with(:projects, :foo, false).
- and_raise(RuntimeError)
+ context 'when a column limit is set' do
+ it 'adds the column with a limit' do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ allow(model).to receive(:transaction).and_yield
+ allow(model).to receive(:update_column_in_batches).with(:projects, :foo, 10)
+ allow(model).to receive(:change_column_null).with(:projects, :foo, false)
+ allow(model).to receive(:change_column_default).with(:projects, :foo, 10)
- expect(model).to receive(:remove_column).
- with(:projects, :foo)
+ expect(model).to receive(:add_column).
+ with(:projects, :foo, :integer, default: nil, limit: 8)
- expect do
- model.add_column_with_default(:projects, :foo, :integer, default: 10)
- end.to raise_error(RuntimeError)
+ model.add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
+ end
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index f12c9a370f7..de68e32e5b4 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1,10 +1,17 @@
require 'spec_helper'
describe Gitlab::GitAccess, lib: true do
- let(:access) { Gitlab::GitAccess.new(actor, project, 'web') }
+ let(:access) { Gitlab::GitAccess.new(actor, project, 'web', authentication_abilities: authentication_abilities) }
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:actor) { user }
+ let(:authentication_abilities) do
+ [
+ :read_project,
+ :download_code,
+ :push_code
+ ]
+ end
describe '#check with single protocols allowed' do
def disable_protocol(protocol)
@@ -15,7 +22,7 @@ describe Gitlab::GitAccess, lib: true do
context 'ssh disabled' do
before do
disable_protocol('ssh')
- @acc = Gitlab::GitAccess.new(actor, project, 'ssh')
+ @acc = Gitlab::GitAccess.new(actor, project, 'ssh', authentication_abilities: authentication_abilities)
end
it 'blocks ssh git push' do
@@ -30,7 +37,7 @@ describe Gitlab::GitAccess, lib: true do
context 'http disabled' do
before do
disable_protocol('http')
- @acc = Gitlab::GitAccess.new(actor, project, 'http')
+ @acc = Gitlab::GitAccess.new(actor, project, 'http', authentication_abilities: authentication_abilities)
end
it 'blocks http push' do
@@ -111,6 +118,36 @@ describe Gitlab::GitAccess, lib: true do
end
end
end
+
+ describe 'build authentication_abilities permissions' do
+ let(:authentication_abilities) { build_authentication_abilities }
+
+ describe 'reporter user' do
+ before { project.team << [user, :reporter] }
+
+ context 'pull code' do
+ it { expect(subject).to be_allowed }
+ end
+ end
+
+ describe 'admin user' do
+ let(:user) { create(:admin) }
+
+ context 'when member of the project' do
+ before { project.team << [user, :reporter] }
+
+ context 'pull code' do
+ it { expect(subject).to be_allowed }
+ end
+ end
+
+ context 'when is not member of the project' do
+ context 'pull code' do
+ it { expect(subject).not_to be_allowed }
+ end
+ end
+ end
+ end
end
describe 'push_access_check' do
@@ -283,38 +320,71 @@ describe Gitlab::GitAccess, lib: true do
end
end
- describe 'deploy key permissions' do
- let(:key) { create(:deploy_key) }
- let(:actor) { key }
+ shared_examples 'can not push code' do
+ subject { access.check('git-receive-pack', '_any') }
+
+ context 'when project is authorized' do
+ before { authorize }
- context 'push code' do
- subject { access.check('git-receive-pack', '_any') }
+ it { expect(subject).not_to be_allowed }
+ end
- context 'when project is authorized' do
- before { key.projects << project }
+ context 'when unauthorized' do
+ context 'to public project' do
+ let(:project) { create(:project, :public) }
it { expect(subject).not_to be_allowed }
end
- context 'when unauthorized' do
- context 'to public project' do
- let(:project) { create(:project, :public) }
+ context 'to internal project' do
+ let(:project) { create(:project, :internal) }
- it { expect(subject).not_to be_allowed }
- end
+ it { expect(subject).not_to be_allowed }
+ end
- context 'to internal project' do
- let(:project) { create(:project, :internal) }
+ context 'to private project' do
+ let(:project) { create(:project) }
- it { expect(subject).not_to be_allowed }
- end
+ it { expect(subject).not_to be_allowed }
+ end
+ end
+ end
- context 'to private project' do
- let(:project) { create(:project, :internal) }
+ describe 'build authentication abilities' do
+ let(:authentication_abilities) { build_authentication_abilities }
- it { expect(subject).not_to be_allowed }
- end
+ it_behaves_like 'can not push code' do
+ def authorize
+ project.team << [user, :reporter]
end
end
end
+
+ describe 'deploy key permissions' do
+ let(:key) { create(:deploy_key) }
+ let(:actor) { key }
+
+ it_behaves_like 'can not push code' do
+ def authorize
+ key.projects << project
+ end
+ end
+ end
+
+ private
+
+ def build_authentication_abilities
+ [
+ :read_project,
+ :build_download_code
+ ]
+ end
+
+ def full_authentication_abilities
+ [
+ :read_project,
+ :download_code,
+ :push_code
+ ]
+ end
end
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 4244b807d41..576cda595bb 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -1,9 +1,16 @@
require 'spec_helper'
describe Gitlab::GitAccessWiki, lib: true do
- let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web') }
+ let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web', authentication_abilities: authentication_abilities) }
let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:authentication_abilities) do
+ [
+ :read_project,
+ :download_code,
+ :push_code
+ ]
+ end
describe 'push_allowed?' do
before do
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
new file mode 100644
index 00000000000..219198eff60
--- /dev/null
+++ b/spec/lib/gitlab/git_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::Git, lib: true do
+ let(:committer_email) { FFaker::Internet.email }
+
+ # I have to remove periods from the end of the name
+ # This happened when the user's name had a suffix (i.e. "Sr.")
+ # This seems to be what git does under the hood. For example, this commit:
+ #
+ # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
+ #
+ # results in this:
+ #
+ # $ git show --pretty
+ # ...
+ # Author: Foo Sr <foo@example.com>
+ # ...
+ let(:committer_name) { FFaker::Name.name.chomp("\.") }
+
+ describe 'committer_hash' do
+ it "returns a hash containing the given email and name" do
+ committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: committer_name)
+
+ expect(committer_hash[:email]).to eq(committer_email)
+ expect(committer_hash[:name]).to eq(committer_name)
+ expect(committer_hash[:time]).to be_a(Time)
+ end
+
+ context 'when email is nil' do
+ it "returns nil" do
+ committer_hash = Gitlab::Git::committer_hash(email: nil, name: committer_name)
+
+ expect(committer_hash).to be_nil
+ end
+ end
+
+ context 'when name is nil' do
+ it "returns nil" do
+ committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: nil)
+
+ expect(committer_hash).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 613c47d55f1..e829b936343 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -66,6 +66,6 @@ describe Gitlab::GithubImport::Client, lib: true do
stub_request(:get, /api.github.com/)
allow(client.api).to receive(:rate_limit!).and_raise(Octokit::NotFound)
- expect { client.issues }.not_to raise_error
+ expect { client.issues {} }.not_to raise_error
end
end
diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
index 9ae02a6c45f..c520a9c53ad 100644
--- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
@@ -73,6 +73,12 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
end
+
+ it 'returns note without created at tag line' do
+ create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(comment.attributes.fetch(:note)).to eq("I'm having a problem with this.")
+ end
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 3fb8de81545..8854c8431b5 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
- let(:label) do
+ let(:label1) do
double(
name: 'Bug',
color: 'ff0000',
@@ -21,6 +21,14 @@ describe Gitlab::GithubImport::Importer, lib: true do
)
end
+ let(:label2) do
+ double(
+ name: nil,
+ color: 'ff0000',
+ url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug'
+ )
+ end
+
let(:milestone) do
double(
number: 1347,
@@ -49,7 +57,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
- url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347'
+ url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347',
+ labels: [double(name: 'Label #1')],
)
end
@@ -67,7 +76,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
- url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348'
+ url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348',
+ labels: [double(name: 'Label #2')],
)
end
@@ -86,18 +96,46 @@ describe Gitlab::GithubImport::Importer, lib: true do
updated_at: updated_at,
closed_at: nil,
merged_at: nil,
- url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
+ url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347',
+ labels: [double(name: 'Label #3')],
+ )
+ end
+
+ let(:release1) do
+ double(
+ tag_name: 'v1.0.0',
+ name: 'First release',
+ body: 'Release v1.0.0',
+ draft: false,
+ created_at: created_at,
+ updated_at: updated_at,
+ url: 'https://api.github.com/repos/octocat/Hello-World/releases/1'
+ )
+ end
+
+ let(:release2) do
+ double(
+ tag_name: 'v2.0.0',
+ name: 'Second release',
+ body: nil,
+ draft: false,
+ created_at: created_at,
+ updated_at: updated_at,
+ url: 'https://api.github.com/repos/octocat/Hello-World/releases/2'
)
end
before do
allow(project).to receive(:import_data).and_return(double.as_null_object)
allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
- allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label, label])
+ allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2])
allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
+ allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([])
+ allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([])
allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
+ allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
end
@@ -113,14 +151,13 @@ describe Gitlab::GithubImport::Importer, lib: true do
error = {
message: 'The remote data could not be fully imported.',
errors: [
- { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title has already been taken" },
+ { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
{ type: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" },
- { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1347", errors: "Invalid Repository. Use user/repo format." },
{ type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" },
- { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
{ type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" },
- { type: :wiki, errors: "Gitlab::Shell::Error" }
- ]
+ { type: :wiki, errors: "Gitlab::Shell::Error" },
+ { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" }
+ ]
}
described_class.new(project).execute
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
index d60c4111e99..c2f1f6b91a1 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -109,6 +109,12 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
end
+
+ it 'returns description without created at tag line' do
+ create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(issue.attributes.fetch(:description)).to eq("I'm having a problem with this.")
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/label_formatter_spec.rb b/spec/lib/gitlab/github_import/label_formatter_spec.rb
index 87593e32db0..8098754d735 100644
--- a/spec/lib/gitlab/github_import/label_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/label_formatter_spec.rb
@@ -1,18 +1,34 @@
require 'spec_helper'
describe Gitlab::GithubImport::LabelFormatter, lib: true do
- describe '#attributes' do
- it 'returns formatted attributes' do
- project = create(:project)
- raw = double(name: 'improvements', color: 'e6e6e6')
+ let(:project) { create(:project) }
+ let(:raw) { double(name: 'improvements', color: 'e6e6e6') }
- formatter = described_class.new(project, raw)
+ subject { described_class.new(project, raw) }
- expect(formatter.attributes).to eq({
+ describe '#attributes' do
+ it 'returns formatted attributes' do
+ expect(subject.attributes).to eq({
project: project,
title: 'improvements',
color: '#e6e6e6'
})
end
end
+
+ describe '#create!' do
+ context 'when label does not exist' do
+ it 'creates a new label' do
+ expect { subject.create! }.to change(Label, :count).by(1)
+ end
+ end
+
+ context 'when label exists' do
+ it 'does not create a new label' do
+ project.labels.create(name: raw.name)
+
+ expect { subject.create! }.not_to change(Label, :count)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
index 014ee462e5c..ab06b7bc5bb 100644
--- a/spec/lib/gitlab/github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/github_import/project_creator_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
)
end
- subject(:service) { described_class.new(repo, namespace, user, github_access_token: 'asdffg') }
+ subject(:service) { described_class.new(repo, repo.name, namespace, user, github_access_token: 'asdffg') }
before do
namespace.add_owner(user)
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index edfc6ad81c6..302f0fc0623 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -140,6 +140,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
end
+
+ it 'returns description without created at tag line' do
+ create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(pull_request.attributes.fetch(:description)).to eq('Please pull these awesome changes')
+ end
end
context 'when it has a milestone' do
diff --git a/spec/lib/gitlab/github_import/release_formatter_spec.rb b/spec/lib/gitlab/github_import/release_formatter_spec.rb
new file mode 100644
index 00000000000..793128c6ab9
--- /dev/null
+++ b/spec/lib/gitlab/github_import/release_formatter_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ReleaseFormatter, lib: true do
+ let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
+ let(:octocat) { double(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+
+ let(:base_data) do
+ {
+ tag_name: 'v1.0.0',
+ name: 'First release',
+ draft: false,
+ created_at: created_at,
+ published_at: created_at,
+ body: 'Release v1.0.0'
+ }
+ end
+
+ subject(:release) { described_class.new(project, raw_data) }
+
+ describe '#attributes' do
+ let(:raw_data) { double(base_data) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ tag: 'v1.0.0',
+ description: 'Release v1.0.0',
+ created_at: created_at,
+ updated_at: created_at
+ }
+
+ expect(release.attributes).to eq(expected)
+ end
+ end
+
+ describe '#valid' do
+ context 'when release is not a draft' do
+ let(:raw_data) { double(base_data) }
+
+ it 'returns true' do
+ expect(release.valid?).to eq true
+ end
+ end
+
+ context 'when release is draft' do
+ let(:raw_data) { double(base_data.merge(draft: true)) }
+
+ it 'returns false' do
+ expect(release.valid?).to eq false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb
index d3f1deb3837..9b499b593d3 100644
--- a/spec/lib/gitlab/gitlab_import/importer_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb
@@ -13,6 +13,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do
'title' => 'Issue',
'description' => 'Lorem ipsum',
'state' => 'opened',
+ 'confidential' => true,
'author' => {
'id' => 283999,
'name' => 'John Doe'
@@ -34,6 +35,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do
title: 'Issue',
description: "*Created by: John Doe*\n\nLorem ipsum",
state: 'opened',
+ confidential: true,
author_id: project.creator_id
}
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
new file mode 100644
index 00000000000..006569254a6
--- /dev/null
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -0,0 +1,187 @@
+---
+issues:
+- subscriptions
+- award_emoji
+- author
+- assignee
+- updated_by
+- milestone
+- notes
+- label_links
+- labels
+- todos
+- user_agent_detail
+- moved_to
+- events
+- merge_requests_closing_issues
+- metrics
+events:
+- author
+- project
+- target
+notes:
+- award_emoji
+- project
+- noteable
+- author
+- updated_by
+- resolved_by
+- todos
+- events
+label_links:
+- target
+- label
+label:
+- subscriptions
+- project
+- lists
+- label_links
+- issues
+- merge_requests
+milestone:
+- project
+- issues
+- labels
+- merge_requests
+- participants
+- events
+snippets:
+- author
+- project
+- notes
+- award_emoji
+releases:
+- project
+project_members:
+- created_by
+- user
+- source
+- project
+merge_requests:
+- subscriptions
+- award_emoji
+- author
+- assignee
+- updated_by
+- milestone
+- notes
+- label_links
+- labels
+- todos
+- target_project
+- source_project
+- merge_user
+- merge_request_diffs
+- merge_request_diff
+- events
+- merge_requests_closing_issues
+- metrics
+merge_request_diff:
+- merge_request
+pipelines:
+- project
+- user
+- statuses
+- builds
+- trigger_requests
+statuses:
+- project
+- pipeline
+- user
+variables:
+- project
+triggers:
+- project
+- trigger_requests
+deploy_keys:
+- user
+- deploy_keys_projects
+- projects
+services:
+- project
+- service_hook
+hooks:
+- project
+protected_branches:
+- project
+- merge_access_levels
+- push_access_levels
+merge_access_levels:
+- protected_branch
+push_access_levels:
+- protected_branch
+project:
+- taggings
+- base_tags
+- tag_taggings
+- tags
+- creator
+- group
+- namespace
+- board
+- last_event
+- services
+- campfire_service
+- drone_ci_service
+- emails_on_push_service
+- builds_email_service
+- irker_service
+- pivotaltracker_service
+- hipchat_service
+- flowdock_service
+- assembla_service
+- asana_service
+- gemnasium_service
+- slack_service
+- buildkite_service
+- bamboo_service
+- teamcity_service
+- pushover_service
+- jira_service
+- redmine_service
+- custom_issue_tracker_service
+- bugzilla_service
+- gitlab_issue_tracker_service
+- external_wiki_service
+- forked_project_link
+- forked_from_project
+- forked_project_links
+- forks
+- merge_requests
+- fork_merge_requests
+- issues
+- labels
+- events
+- milestones
+- notes
+- snippets
+- hooks
+- protected_branches
+- project_members
+- users
+- requesters
+- deploy_keys_projects
+- deploy_keys
+- users_star_projects
+- starrers
+- releases
+- lfs_objects_projects
+- lfs_objects
+- project_group_links
+- invited_groups
+- todos
+- notification_settings
+- import_data
+- commit_statuses
+- pipelines
+- builds
+- runner_projects
+- runners
+- variables
+- triggers
+- environments
+- deployments
+- project_feature
+award_emoji:
+- awardable
+- user \ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
new file mode 100644
index 00000000000..2ba344092ce
--- /dev/null
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+# Part of the test security suite for the Import/Export feature
+# Checks whether there are new attributes in models that are currently being exported as part of the
+# project Import/Export feature.
+# If there are new attributes, these will have to either be added to this spec in case we want them
+# to be included as part of the export, or blacklist them using the import_export.yml configuration file.
+# Likewise, new models added to import_export.yml, will need to be added with their correspondent attributes
+# to this spec.
+describe 'Import/Export attribute configuration', lib: true do
+ include ConfigurationHelper
+
+ let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+ let(:relation_names) do
+ names = names_from_tree(config_hash['project_tree'])
+
+ # Remove duplicated or add missing models
+ # - project is not part of the tree, so it has to be added manually.
+ # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
+ names.flatten.uniq - ['milestones', 'labels'] + ['project']
+ end
+
+ let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' }
+ let(:safe_model_attributes) { YAML.load_file(safe_attributes_file) }
+
+ it 'has no new columns' do
+ relation_names.each do |relation_name|
+ relation_class = relation_class_for_name(relation_name)
+
+ expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class.to_s} to exist in safe_model_attributes"
+
+ current_attributes = parsed_attributes(relation_name, relation_class.attribute_names)
+ safe_attributes = safe_model_attributes[relation_class.to_s]
+ new_attributes = current_attributes - safe_attributes
+
+ expect(new_attributes).to be_empty, failure_message(relation_class.to_s, new_attributes)
+ end
+ end
+
+ def failure_message(relation_class, new_attributes)
+ <<-MSG
+ It looks like #{relation_class}, which is exported using the project Import/Export, has new attributes: #{new_attributes.join(',')}
+
+ Please add the attribute(s) to SAFE_MODEL_ATTRIBUTES if you consider this can be exported.
+ Otherwise, please blacklist the attribute(s) in IMPORT_EXPORT_CONFIG by adding it to its correspondent
+ model in the +excluded_attributes+ section.
+
+ SAFE_MODEL_ATTRIBUTES: #{File.expand_path(safe_attributes_file)}
+ IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
+ MSG
+ end
+
+ class Author < User
+ end
+end
diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb
new file mode 100644
index 00000000000..9b492d1b9c7
--- /dev/null
+++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+# Part of the test security suite for the Import/Export feature
+# Finds if a new model has been added that can potentially be part of the Import/Export
+# If it finds a new model, it will show a +failure_message+ with the options available.
+describe 'Import/Export model configuration', lib: true do
+ include ConfigurationHelper
+
+ let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+ let(:model_names) do
+ names = names_from_tree(config_hash['project_tree'])
+
+ # Remove duplicated or add missing models
+ # - project is not part of the tree, so it has to be added manually.
+ # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
+ # - User, Author... Models we do not care about for checking models
+ names.flatten.uniq - ['milestones', 'labels', 'user', 'author'] + ['project']
+ end
+
+ let(:all_models_yml) { 'spec/lib/gitlab/import_export/all_models.yml' }
+ let(:all_models) { YAML.load_file(all_models_yml) }
+ let(:current_models) { setup_models }
+
+ it 'has no new models' do
+ model_names.each do |model_name|
+ new_models = Array(current_models[model_name]) - Array(all_models[model_name])
+ expect(new_models).to be_empty, failure_message(model_name.classify, new_models)
+ end
+ end
+
+ # List of current models between models, in the format of
+ # {model: [model_2, model3], ...}
+ def setup_models
+ all_models_hash = {}
+
+ model_names.each do |model_name|
+ model_class = relation_class_for_name(model_name)
+
+ all_models_hash[model_name] = associations_for(model_class) - ['project']
+ end
+
+ all_models_hash
+ end
+
+ def failure_message(parent_model_name, new_models)
+ <<-MSG
+ New model(s) <#{new_models.join(',')}> have been added, related to #{parent_model_name}, which is exported by
+ the Import/Export feature.
+
+ If you think this model should be included in the export, please add it to IMPORT_EXPORT_CONFIG.
+ Definitely add it to MODELS_JSON to signal that you've handled this error and to prevent it from showing up in the future.
+
+ MODELS_JSON: #{File.expand_path(all_models_yml)}
+ IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
+ MSG
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 5114f9c55e1..98323fe6be4 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -24,7 +24,7 @@
"test_ee_field": "test",
"milestone": {
"id": 1,
- "title": "v0.0",
+ "title": "test milestone",
"project_id": 8,
"description": "test milestone",
"due_date": null,
@@ -51,7 +51,7 @@
{
"id": 2,
"label_id": 2,
- "target_id": 3,
+ "target_id": 40,
"target_type": "Issue",
"created_at": "2016-07-22T08:57:02.840Z",
"updated_at": "2016-07-22T08:57:02.840Z",
@@ -281,6 +281,31 @@
"deleted_at": null,
"due_date": null,
"moved_to_id": null,
+ "milestone": {
+ "id": 1,
+ "title": "test milestone",
+ "project_id": 8,
+ "description": "test milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "events": [
+ {
+ "id": 487,
+ "target_type": "Milestone",
+ "target_id": 1,
+ "title": null,
+ "data": null,
+ "project_id": 46,
+ "created_at": "2016-06-14T15:02:04.418Z",
+ "updated_at": "2016-06-14T15:02:04.418Z",
+ "action": 1,
+ "author_id": 18
+ }
+ ]
+ },
"notes": [
{
"id": 359,
@@ -494,6 +519,27 @@
"deleted_at": null,
"due_date": null,
"moved_to_id": null,
+ "label_links": [
+ {
+ "id": 99,
+ "label_id": 2,
+ "target_id": 38,
+ "target_type": "Issue",
+ "created_at": "2016-07-22T08:57:02.840Z",
+ "updated_at": "2016-07-22T08:57:02.840Z",
+ "label": {
+ "id": 2,
+ "title": "test2",
+ "color": "#428bca",
+ "project_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "priority": null
+ }
+ }
+ ],
"notes": [
{
"id": 367,
@@ -2186,6 +2232,31 @@
],
"milestones": [
{
+ "id": 1,
+ "title": "test milestone",
+ "project_id": 8,
+ "description": "test milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "events": [
+ {
+ "id": 487,
+ "target_type": "Milestone",
+ "target_id": 1,
+ "title": null,
+ "data": null,
+ "project_id": 46,
+ "created_at": "2016-06-14T15:02:04.418Z",
+ "updated_at": "2016-06-14T15:02:04.418Z",
+ "action": 1,
+ "author_id": 18
+ }
+ ]
+ },
+ {
"id": 20,
"title": "v4.0",
"project_id": 5,
@@ -6478,7 +6549,7 @@
{
"id": 37,
"project_id": 5,
- "ref": "master",
+ "ref": null,
"sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"before_sha": null,
"push_data": null,
@@ -6872,6 +6943,7 @@
"note_events": true,
"build_events": true,
"category": "issue_tracker",
+ "type": "CustomIssueTrackerService",
"default": true,
"wiki_page_events": true
},
@@ -7301,6 +7373,41 @@
],
"protected_branches": [
-
- ]
-}
+ {
+ "id": 1,
+ "project_id": 9,
+ "name": "master",
+ "created_at": "2016-08-30T07:32:52.426Z",
+ "updated_at": "2016-08-30T07:32:52.426Z",
+ "merge_access_levels": [
+ {
+ "id": 1,
+ "protected_branch_id": 1,
+ "access_level": 40,
+ "created_at": "2016-08-30T07:32:52.458Z",
+ "updated_at": "2016-08-30T07:32:52.458Z"
+ }
+ ],
+ "push_access_levels": [
+ {
+ "id": 1,
+ "protected_branch_id": 1,
+ "access_level": 40,
+ "created_at": "2016-08-30T07:32:52.490Z",
+ "updated_at": "2016-08-30T07:32:52.490Z"
+ }
+ ]
+ }
+ ],
+ "project_feature": {
+ "builds_access_level": 0,
+ "created_at": "2014-12-26T09:26:45.000Z",
+ "id": 2,
+ "issues_access_level": 0,
+ "merge_requests_access_level": 20,
+ "project_id": 4,
+ "snippets_access_level": 20,
+ "updated_at": "2016-09-23T11:58:28.000Z",
+ "wiki_access_level": 20
+ }
+} \ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index a07ef279e68..7582a732cdf 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -29,12 +29,30 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED)
end
+ it 'has the same label associated to two issues' do
+ restored_project_json
+
+ expect(Label.first.issues.count).to eq(2)
+ end
+
+ it 'has milestones associated to two separate issues' do
+ restored_project_json
+
+ expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
+ end
+
it 'creates a valid pipeline note' do
restored_project_json
expect(Ci::Pipeline.first.notes).not_to be_empty
end
+ it 'restores pipelines with missing ref' do
+ restored_project_json
+
+ expect(Ci::Pipeline.where(ref: nil)).not_to be_empty
+ end
+
it 'restores the correct event with symbolised data' do
restored_project_json
@@ -49,6 +67,18 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
end
+ it 'contains the merge access levels on a protected branch' do
+ restored_project_json
+
+ expect(ProtectedBranch.first.merge_access_levels).not_to be_empty
+ end
+
+ it 'contains the push access levels on a protected branch' do
+ restored_project_json
+
+ expect(ProtectedBranch.first.push_access_levels).not_to be_empty
+ end
+
context 'event at forth level of the tree' do
let(:event) { Event.where(title: 'test levels').first }
@@ -77,10 +107,16 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Label.first.label_links.first.target).not_to be_nil
end
- it 'has milestones associated to issues' do
+ it 'has a project feature' do
+ restored_project_json
+
+ expect(project.project_feature).not_to be_nil
+ end
+
+ it 'restores the correct service' do
restored_project_json
- expect(Milestone.find_by_description('test milestone').issues).not_to be_empty
+ expect(CustomIssueTrackerService.first).not_to be_nil
end
context 'Merge requests' do
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index d891c2d0cc6..cf8f2200c57 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -111,6 +111,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty
end
+ it 'saves the correct service type' do
+ expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService')
+ end
+
it 'has project feature' do
project_feature = saved_project_json['project_feature']
expect(project_feature).not_to be_empty
@@ -161,6 +165,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
commit_id: ci_pipeline.sha)
create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+ create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
new file mode 100644
index 00000000000..8bccd313d6c
--- /dev/null
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -0,0 +1,330 @@
+---
+Issue:
+- id
+- title
+- assignee_id
+- author_id
+- project_id
+- created_at
+- updated_at
+- position
+- branch_name
+- description
+- state
+- iid
+- updated_by_id
+- confidential
+- deleted_at
+- due_date
+- moved_to_id
+- lock_version
+- milestone_id
+- weight
+Event:
+- id
+- target_type
+- target_id
+- title
+- data
+- project_id
+- created_at
+- updated_at
+- action
+- author_id
+Note:
+- id
+- note
+- noteable_type
+- author_id
+- created_at
+- updated_at
+- project_id
+- attachment
+- line_code
+- commit_id
+- noteable_id
+- system
+- st_diff
+- updated_by_id
+- type
+- position
+- original_position
+- resolved_at
+- resolved_by_id
+- discussion_id
+- original_discussion_id
+LabelLink:
+- id
+- label_id
+- target_id
+- target_type
+- created_at
+- updated_at
+Label:
+- id
+- title
+- color
+- project_id
+- created_at
+- updated_at
+- template
+- description
+- priority
+Milestone:
+- id
+- title
+- project_id
+- description
+- due_date
+- created_at
+- updated_at
+- state
+- iid
+ProjectSnippet:
+- id
+- title
+- content
+- author_id
+- project_id
+- created_at
+- updated_at
+- file_name
+- type
+- visibility_level
+Release:
+- id
+- tag
+- description
+- project_id
+- created_at
+- updated_at
+ProjectMember:
+- id
+- access_level
+- source_id
+- source_type
+- user_id
+- notification_level
+- type
+- created_at
+- updated_at
+- created_by_id
+- invite_email
+- invite_token
+- invite_accepted_at
+- requested_at
+- expires_at
+User:
+- id
+- username
+- email
+MergeRequest:
+- id
+- target_branch
+- source_branch
+- source_project_id
+- author_id
+- assignee_id
+- title
+- created_at
+- updated_at
+- state
+- merge_status
+- target_project_id
+- iid
+- description
+- position
+- locked_at
+- updated_by_id
+- merge_error
+- merge_params
+- merge_when_build_succeeds
+- merge_user_id
+- merge_commit_sha
+- deleted_at
+- in_progress_merge_commit_sha
+- lock_version
+- milestone_id
+- approvals_before_merge
+- rebase_commit_sha
+MergeRequestDiff:
+- id
+- state
+- st_commits
+- merge_request_id
+- created_at
+- updated_at
+- base_commit_sha
+- real_size
+- head_commit_sha
+- start_commit_sha
+Ci::Pipeline:
+- id
+- project_id
+- ref
+- sha
+- before_sha
+- push_data
+- created_at
+- updated_at
+- tag
+- yaml_errors
+- committed_at
+- gl_project_id
+- status
+- started_at
+- finished_at
+- duration
+- user_id
+CommitStatus:
+- id
+- project_id
+- status
+- finished_at
+- trace
+- created_at
+- updated_at
+- started_at
+- runner_id
+- coverage
+- commit_id
+- commands
+- job_id
+- name
+- deploy
+- options
+- allow_failure
+- stage
+- trigger_request_id
+- stage_idx
+- tag
+- ref
+- user_id
+- type
+- target_url
+- description
+- artifacts_file
+- gl_project_id
+- artifacts_metadata
+- erased_by_id
+- erased_at
+- artifacts_expire_at
+- environment
+- artifacts_size
+- when
+- yaml_variables
+- queued_at
+- token
+Ci::Variable:
+- id
+- project_id
+- key
+- value
+- encrypted_value
+- encrypted_value_salt
+- encrypted_value_iv
+- gl_project_id
+Ci::Trigger:
+- id
+- token
+- project_id
+- deleted_at
+- created_at
+- updated_at
+- gl_project_id
+DeployKey:
+- id
+- user_id
+- created_at
+- updated_at
+- key
+- title
+- type
+- fingerprint
+- public
+Service:
+- id
+- type
+- title
+- project_id
+- created_at
+- updated_at
+- active
+- properties
+- template
+- push_events
+- issues_events
+- merge_requests_events
+- tag_push_events
+- note_events
+- pipeline_events
+- build_events
+- category
+- default
+- wiki_page_events
+- confidential_issues_events
+ProjectHook:
+- id
+- url
+- project_id
+- created_at
+- updated_at
+- type
+- service_id
+- push_events
+- issues_events
+- merge_requests_events
+- tag_push_events
+- note_events
+- pipeline_events
+- enable_ssl_verification
+- build_events
+- wiki_page_events
+- token
+- group_id
+- confidential_issues_events
+ProtectedBranch:
+- id
+- project_id
+- name
+- created_at
+- updated_at
+Project:
+- description
+- issues_enabled
+- merge_requests_enabled
+- wiki_enabled
+- snippets_enabled
+- visibility_level
+- archived
+Author:
+- name
+ProjectFeature:
+- id
+- project_id
+- merge_requests_access_level
+- issues_access_level
+- wiki_access_level
+- snippets_access_level
+- builds_access_level
+- created_at
+- updated_at
+ProtectedBranch::MergeAccessLevel:
+- id
+- protected_branch_id
+- access_level
+- created_at
+- updated_at
+ProtectedBranch::PushAccessLevel:
+- id
+- protected_branch_id
+- access_level
+- created_at
+- updated_at
+AwardEmoji:
+- id
+- user_id
+- name
+- awardable_type
+- created_at
+- updated_at
diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb
index 90c6d1c67f6..c680e712b59 100644
--- a/spec/lib/gitlab/import_export/version_checker_spec.rb
+++ b/spec/lib/gitlab/import_export/version_checker_spec.rb
@@ -23,7 +23,7 @@ describe Gitlab::ImportExport::VersionChecker, services: true do
it 'shows the correct error message' do
described_class.check!(shared: shared)
- expect(shared.errors.first).to eq("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
+ expect(shared.errors.first).to eq("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
end
end
end
diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb
index 4847b5f3b0e..563c074017a 100644
--- a/spec/lib/gitlab/ldap/adapter_spec.rb
+++ b/spec/lib/gitlab/ldap/adapter_spec.rb
@@ -1,24 +1,105 @@
require 'spec_helper'
describe Gitlab::LDAP::Adapter, lib: true do
- let(:adapter) { Gitlab::LDAP::Adapter.new 'ldapmain' }
+ include LdapHelpers
+
+ let(:ldap) { double(:ldap) }
+ let(:adapter) { ldap_adapter('ldapmain', ldap) }
+
+ describe '#users' do
+ before do
+ stub_ldap_config(base: 'dc=example,dc=com')
+ end
+
+ it 'searches with the proper options when searching by uid' do
+ # Requires this expectation style to match the filter
+ expect(adapter).to receive(:ldap_search) do |arg|
+ expect(arg[:filter].to_s).to eq('(uid=johndoe)')
+ expect(arg[:base]).to eq('dc=example,dc=com')
+ expect(arg[:attributes]).to match(%w{uid cn mail dn})
+ end.and_return({})
+
+ adapter.users('uid', 'johndoe')
+ end
+
+ it 'searches with the proper options when searching by dn' do
+ expect(adapter).to receive(:ldap_search).with(
+ base: 'uid=johndoe,ou=users,dc=example,dc=com',
+ scope: Net::LDAP::SearchScope_BaseObject,
+ attributes: %w{uid cn mail dn},
+ filter: nil
+ ).and_return({})
+
+ adapter.users('dn', 'uid=johndoe,ou=users,dc=example,dc=com')
+ end
+
+ it 'searches with the proper options when searching with a limit' do
+ expect(adapter)
+ .to receive(:ldap_search).with(hash_including(size: 100)).and_return({})
+
+ adapter.users('uid', 'johndoe', 100)
+ end
+
+ it 'returns an LDAP::Person if search returns a result' do
+ entry = ldap_user_entry('johndoe')
+ allow(adapter).to receive(:ldap_search).and_return([entry])
+
+ results = adapter.users('uid', 'johndoe')
+
+ expect(results.size).to eq(1)
+ expect(results.first.uid).to eq('johndoe')
+ end
+
+ it 'returns empty array if search entry does not respond to uid' do
+ entry = Net::LDAP::Entry.new
+ entry['dn'] = user_dn('johndoe')
+ allow(adapter).to receive(:ldap_search).and_return([entry])
+
+ results = adapter.users('uid', 'johndoe')
+
+ expect(results).to be_empty
+ end
+
+ it 'uses the right uid attribute when non-default' do
+ stub_ldap_config(uid: 'sAMAccountName')
+ expect(adapter).to receive(:ldap_search).with(
+ hash_including(attributes: %w{sAMAccountName cn mail dn})
+ ).and_return({})
+
+ adapter.users('sAMAccountName', 'johndoe')
+ end
+ end
describe '#dn_matches_filter?' do
- let(:ldap) { double(:ldap) }
subject { adapter.dn_matches_filter?(:dn, :filter) }
- before { allow(adapter).to receive(:ldap).and_return(ldap) }
+
+ context "when the search result is non-empty" do
+ before { allow(adapter).to receive(:ldap_search).and_return([:foo]) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context "when the search result is empty" do
+ before { allow(adapter).to receive(:ldap_search).and_return([]) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#ldap_search' do
+ subject { adapter.ldap_search(base: :dn, filter: :filter) }
context "when the search is successful" do
context "and the result is non-empty" do
before { allow(ldap).to receive(:search).and_return([:foo]) }
- it { is_expected.to be_truthy }
+ it { is_expected.to eq [:foo] }
end
context "and the result is empty" do
before { allow(ldap).to receive(:search).and_return([]) }
- it { is_expected.to be_falsey }
+ it { is_expected.to eq [] }
end
end
@@ -30,7 +111,22 @@ describe Gitlab::LDAP::Adapter, lib: true do
)
end
- it { is_expected.to be_falsey }
+ it { is_expected.to eq [] }
+ end
+
+ context "when the search raises an LDAP exception" do
+ before do
+ allow(ldap).to receive(:search) { raise Net::LDAP::Error, "some error" }
+ allow(Rails.logger).to receive(:warn)
+ end
+
+ it { is_expected.to eq [] }
+
+ it 'logs the error' do
+ subject
+ expect(Rails.logger).to have_received(:warn).with(
+ "LDAP search raised exception Net::LDAP::Error: some error")
+ end
end
end
end
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
new file mode 100644
index 00000000000..e9c1163e22a
--- /dev/null
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::LfsToken, lib: true do
+ describe '#token' do
+ shared_examples 'an LFS token generator' do
+ it 'returns a randomly generated token' do
+ token = handler.token
+
+ expect(token).not_to be_nil
+ expect(token).to be_a String
+ expect(token.length).to eq 50
+ end
+
+ it 'returns the correct token based on the key' do
+ token = handler.token
+
+ expect(handler.token).to eq(token)
+ end
+ end
+
+ context 'when the actor is a user' do
+ let(:actor) { create(:user) }
+ let(:handler) { described_class.new(actor) }
+
+ it_behaves_like 'an LFS token generator'
+
+ it 'returns the correct username' do
+ expect(handler.actor_name).to eq(actor.username)
+ end
+
+ it 'returns the correct token type' do
+ expect(handler.type).to eq(:lfs_token)
+ end
+ end
+
+ context 'when the actor is a deploy key' do
+ let(:actor) { create(:deploy_key) }
+ let(:handler) { described_class.new(actor) }
+
+ it_behaves_like 'an LFS token generator'
+
+ it 'returns the correct username' do
+ expect(handler.actor_name).to eq("lfs+deploy-key-#{actor.id}")
+ end
+
+ it 'returns the correct token type' do
+ expect(handler.type).to eq(:lfs_deploy_token)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index e8b236426e9..4ae216d55b0 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -40,4 +40,13 @@ describe 'Gitlab::Popen', lib: true, no_db: true do
it { expect(@status).to be_zero }
it { expect(@output).to include('spec') }
end
+
+ context 'use stdin' do
+ before do
+ @output, @status = @klass.new.popen(%w[cat]) { |stdin| stdin.write 'hello' }
+ end
+
+ it { expect(@status).to be_zero }
+ it { expect(@output).to eq('hello') }
+ end
end
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index e54f5ffb312..cb54c020b31 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -3,19 +3,27 @@ require 'spec_helper'
describe Gitlab::Redis do
let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s }
- before(:each) { described_class.reset_params! }
- after(:each) { described_class.reset_params! }
+ before(:each) { clear_raw_config }
+ after(:each) { clear_raw_config }
describe '.params' do
subject { described_class.params }
+ it 'withstands mutation' do
+ params1 = described_class.params
+ params2 = described_class.params
+ params1[:foo] = :bar
+
+ expect(params2).not_to have_key(:foo)
+ end
+
context 'when url contains unix socket reference' do
let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s }
let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s }
context 'with old format' do
it 'returns path key instead' do
- expect_any_instance_of(described_class).to receive(:config_file) { config_old }
+ stub_const("#{described_class}::CONFIG_FILE", config_old)
is_expected.to include(path: '/path/to/old/redis.sock')
is_expected.not_to have_key(:url)
@@ -24,7 +32,7 @@ describe Gitlab::Redis do
context 'with new format' do
it 'returns path key instead' do
- expect_any_instance_of(described_class).to receive(:config_file) { config_new }
+ stub_const("#{described_class}::CONFIG_FILE", config_new)
is_expected.to include(path: '/path/to/redis.sock')
is_expected.not_to have_key(:url)
@@ -38,7 +46,7 @@ describe Gitlab::Redis do
context 'with old format' do
it 'returns hash with host, port, db, and password' do
- expect_any_instance_of(described_class).to receive(:config_file) { config_old }
+ stub_const("#{described_class}::CONFIG_FILE", config_old)
is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99)
is_expected.not_to have_key(:url)
@@ -47,7 +55,7 @@ describe Gitlab::Redis do
context 'with new format' do
it 'returns hash with host, port, db, and password' do
- expect_any_instance_of(described_class).to receive(:config_file) { config_new }
+ stub_const("#{described_class}::CONFIG_FILE", config_new)
is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99)
is_expected.not_to have_key(:url)
@@ -56,6 +64,30 @@ describe Gitlab::Redis do
end
end
+ describe '.url' do
+ it 'withstands mutation' do
+ url1 = described_class.url
+ url2 = described_class.url
+ url1 << 'foobar'
+
+ expect(url2).not_to end_with('foobar')
+ end
+ end
+
+ describe '._raw_config' do
+ subject { described_class._raw_config }
+
+ it 'should be frozen' do
+ expect(subject).to be_frozen
+ end
+
+ it 'returns false when the file does not exist' do
+ stub_const("#{described_class}::CONFIG_FILE", '/var/empty/doesnotexist')
+
+ expect(subject).to eq(false)
+ end
+ end
+
describe '#raw_config_hash' do
it 'returns default redis url when no config file is present' do
expect(subject).to receive(:fetch_config) { false }
@@ -71,9 +103,15 @@ describe Gitlab::Redis do
describe '#fetch_config' do
it 'returns false when no config file is present' do
- allow(File).to receive(:exist?).with(redis_config) { false }
+ allow(described_class).to receive(:_raw_config) { false }
expect(subject.send(:fetch_config)).to be_falsey
end
end
+
+ def clear_raw_config
+ described_class.remove_instance_variable(:@_raw_config)
+ rescue NameError
+ # raised if @_raw_config was not set; ignore
+ end
end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 8a656ab0ee9..dfbefad6367 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -12,12 +12,6 @@ describe Gitlab::SearchResults do
let!(:milestone) { create(:milestone, project: project, title: 'foo') }
let(:results) { described_class.new(user, Project.all, 'foo') }
- describe '#total_count' do
- it 'returns the total amount of search hits' do
- expect(results.total_count).to eq(4)
- end
- end
-
describe '#projects_count' do
it 'returns the total amount of projects' do
expect(results.projects_count).to eq(1)
@@ -42,18 +36,6 @@ describe Gitlab::SearchResults do
end
end
- describe '#empty?' do
- it 'returns true when there are no search results' do
- allow(results).to receive(:total_count).and_return(0)
-
- expect(results.empty?).to eq(true)
- end
-
- it 'returns false when there are search results' do
- expect(results.empty?).to eq(false)
- end
- end
-
describe 'confidential issues' do
let(:project_1) { create(:empty_project) }
let(:project_2) { create(:empty_project) }
diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb
index e86b9ef6a63..b661a894c0c 100644
--- a/spec/lib/gitlab/snippet_search_results_spec.rb
+++ b/spec/lib/gitlab/snippet_search_results_spec.rb
@@ -5,12 +5,6 @@ describe Gitlab::SnippetSearchResults do
let(:results) { described_class.new(Snippet.all, 'foo') }
- describe '#total_count' do
- it 'returns the total amount of search hits' do
- expect(results.total_count).to eq(2)
- end
- end
-
describe '#snippet_titles_count' do
it 'returns the amount of matched snippet titles' do
expect(results.snippet_titles_count).to eq(1)
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
index f770857e958..d2d334e6413 100644
--- a/spec/lib/gitlab/template/issue_template_spec.rb
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::Template::IssueTemplate do
let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
before do
- project.team.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
@@ -53,7 +53,7 @@ describe Gitlab::Template::IssueTemplate do
context 'when repo is bare or empty' do
let(:empty_project) { create(:empty_project) }
- before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+ before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "returns empty array" do
templates = subject.by_category('', empty_project)
@@ -78,7 +78,7 @@ describe Gitlab::Template::IssueTemplate do
context "when repo is empty" do
let(:empty_project) { create(:empty_project) }
- before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+ before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "raises file not found" do
issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project)
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
index bb0f68043fa..ddf68c4cf78 100644
--- a/spec/lib/gitlab/template/merge_request_template_spec.rb
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::Template::MergeRequestTemplate do
let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
before do
- project.team.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
@@ -53,7 +53,7 @@ describe Gitlab::Template::MergeRequestTemplate do
context 'when repo is bare or empty' do
let(:empty_project) { create(:empty_project) }
- before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+ before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "returns empty array" do
templates = subject.by_category('', empty_project)
@@ -78,7 +78,7 @@ describe Gitlab::Template::MergeRequestTemplate do
context "when repo is empty" do
let(:empty_project) { create(:empty_project) }
- before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+ before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "raises file not found" do
issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project)
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index c5c1402e8fc..b5b685da904 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -1,18 +1,141 @@
require 'spec_helper'
describe Gitlab::Workhorse, lib: true do
- let(:project) { create(:project) }
- let(:subject) { Gitlab::Workhorse }
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
- describe "#send_git_archive" do
+ def decode_workhorse_header(array)
+ key, value = array
+ command, encoded_params = value.split(":")
+ params = JSON.parse(Base64.urlsafe_decode64(encoded_params))
+
+ [key, command, params]
+ end
+
+ describe ".send_git_archive" do
context "when the repository doesn't have an archive file path" do
before do
allow(project.repository).to receive(:archive_metadata).and_return(Hash.new)
end
it "raises an error" do
- expect { subject.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError)
+ expect { described_class.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError)
+ end
+ end
+ end
+
+ describe '.send_git_patch' do
+ let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
+ subject { described_class.send_git_patch(repository, diff_refs) }
+
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-format-patch")
+ expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+ end
+ end
+
+ describe '.send_git_diff' do
+ let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
+ subject { described_class.send_git_patch(repository, diff_refs) }
+
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-format-patch")
+ expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+ end
+ end
+
+ describe ".secret" do
+ subject { described_class.secret }
+
+ before do
+ described_class.instance_variable_set(:@secret, nil)
+ described_class.write_secret
+ end
+
+ it 'returns 32 bytes' do
+ expect(subject).to be_a(String)
+ expect(subject.length).to eq(32)
+ expect(subject.encoding).to eq(Encoding::ASCII_8BIT)
+ end
+
+ it 'accepts a trailing newline' do
+ open(described_class.secret_path, 'a') { |f| f.write "\n" }
+ expect(subject.length).to eq(32)
+ end
+
+ it 'raises an exception if the secret file cannot be read' do
+ File.delete(described_class.secret_path)
+ expect { subject }.to raise_exception(Errno::ENOENT)
+ end
+
+ it 'raises an exception if the secret file contains the wrong number of bytes' do
+ File.truncate(described_class.secret_path, 0)
+ expect { subject }.to raise_exception(RuntimeError)
+ end
+ end
+
+ describe ".write_secret" do
+ let(:secret_path) { described_class.secret_path }
+ before do
+ begin
+ File.delete(secret_path)
+ rescue Errno::ENOENT
end
+
+ described_class.write_secret
+ end
+
+ it 'uses mode 0600' do
+ expect(File.stat(secret_path).mode & 0777).to eq(0600)
+ end
+
+ it 'writes base64 data' do
+ bytes = Base64.strict_decode64(File.read(secret_path))
+ expect(bytes).not_to be_empty
+ end
+ end
+
+ describe '#verify_api_request!' do
+ let(:header_key) { described_class::INTERNAL_API_REQUEST_HEADER }
+ let(:payload) { { 'iss' => 'gitlab-workhorse' } }
+
+ it 'accepts a correct header' do
+ headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') }
+ expect { call_verify(headers) }.not_to raise_error
+ end
+
+ it 'raises an error when the header is not set' do
+ expect { call_verify({}) }.to raise_jwt_error
+ end
+
+ it 'raises an error when the header is not signed' do
+ headers = { header_key => JWT.encode(payload, nil, 'none') }
+ expect { call_verify(headers) }.to raise_jwt_error
+ end
+
+ it 'raises an error when the header is signed with the wrong key' do
+ headers = { header_key => JWT.encode(payload, 'wrongkey', 'HS256') }
+ expect { call_verify(headers) }.to raise_jwt_error
+ end
+
+ it 'raises an error when the issuer is incorrect' do
+ payload['iss'] = 'somebody else'
+ headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') }
+ expect { call_verify(headers) }.to raise_jwt_error
+ end
+
+ def raise_jwt_error
+ raise_error(JWT::DecodeError)
+ end
+
+ def call_verify(headers)
+ described_class.verify_api_request!(headers)
end
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index eae9c060c38..cd8578b6f49 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -402,7 +402,7 @@ describe Notify do
describe 'project access requested' do
context 'for a project in a user namespace' do
- let(:project) { create(:project).tap { |p| p.team << [p.owner, :master, p.owner] } }
+ let(:project) { create(:project, :public).tap { |p| p.team << [p.owner, :master, p.owner] } }
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -429,7 +429,7 @@ describe Notify do
context 'for a project in a group' do
let(:group_owner) { create(:user) }
let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } }
- let(:project) { create(:project, namespace: group) }
+ let(:project) { create(:project, :public, namespace: group) }
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -492,21 +492,22 @@ describe Notify do
end
end
- def invite_to_project(project:, email:, inviter:)
- Member.add_user(
- project.project_members,
- 'toto@example.com',
- Gitlab::Access::DEVELOPER,
- current_user: inviter
+ def invite_to_project(project, inviter:)
+ create(
+ :project_member,
+ :developer,
+ project: project,
+ invite_token: '1234',
+ invite_email: 'toto@example.com',
+ user: nil,
+ created_by: inviter
)
-
- project.project_members.invite.last
end
describe 'project invitation' do
let(:project) { create(:project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
- let(:project_member) { invite_to_project(project: project, email: 'toto@example.com', inviter: master) }
+ let(:project_member) { invite_to_project(project, inviter: master) }
subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) }
@@ -525,10 +526,10 @@ describe Notify do
describe 'project invitation accepted' do
let(:project) { create(:project) }
- let(:invited_user) { create(:user) }
+ let(:invited_user) { create(:user, name: 'invited user') }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do
- invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+ invitee = invite_to_project(project, inviter: master)
invitee.accept_invite!(invited_user)
invitee
end
@@ -552,7 +553,7 @@ describe Notify do
let(:project) { create(:project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do
- invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+ invitee = invite_to_project(project, inviter: master)
invitee.decline_invite!
invitee
end
@@ -744,21 +745,22 @@ describe Notify do
end
end
- def invite_to_group(group:, email:, inviter:)
- Member.add_user(
- group.group_members,
- 'toto@example.com',
- Gitlab::Access::DEVELOPER,
- current_user: inviter
+ def invite_to_group(group, inviter:)
+ create(
+ :group_member,
+ :developer,
+ group: group,
+ invite_token: '1234',
+ invite_email: 'toto@example.com',
+ user: nil,
+ created_by: inviter
)
-
- group.group_members.invite.last
end
describe 'group invitation' do
let(:group) { create(:group) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
- let(:group_member) { invite_to_group(group: group, email: 'toto@example.com', inviter: owner) }
+ let(:group_member) { invite_to_group(group, inviter: owner) }
subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) }
@@ -777,10 +779,10 @@ describe Notify do
describe 'group invitation accepted' do
let(:group) { create(:group) }
- let(:invited_user) { create(:user) }
+ let(:invited_user) { create(:user, name: 'invited user') }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) do
- invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+ invitee = invite_to_group(group, inviter: owner)
invitee.accept_invite!(invited_user)
invitee
end
@@ -804,7 +806,7 @@ describe Notify do
let(:group) { create(:group) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) do
- invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+ invitee = invite_to_group(group, inviter: owner)
invitee.decline_invite!
invitee
end
@@ -861,7 +863,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :create) }
it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
@@ -914,7 +916,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :delete) }
it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
@@ -936,7 +938,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
@@ -964,7 +966,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) }
it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
@@ -1066,7 +1068,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) }
it_behaves_like 'it should show Gmail Actions View Commit link'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb
index 93de5850ba2..5c9851f14c7 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/mailers/shared/notify.rb
@@ -169,10 +169,19 @@ shared_examples 'it should show Gmail Actions View Commit link' do
end
shared_examples 'an unsubscribeable thread' do
+ it 'has a List-Unsubscribe header in the correct format' do
+ is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
+ is_expected.to have_header 'List-Unsubscribe', /^<.+>$/
+ end
+
it { is_expected.to have_body_text /unsubscribe/ }
end
shared_examples 'a user cannot unsubscribe through footer link' do
+ it 'does not have a List-Unsubscribe header' do
+ is_expected.not_to have_header 'List-Unsubscribe', /unsubscribe/
+ end
+
it { is_expected.not_to have_body_text /unsubscribe/ }
end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index cee20234e1f..03d02b4d382 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -1,3 +1,4 @@
+# encoding: utf-8
require 'rails_helper'
describe Blob do
@@ -7,6 +8,25 @@ describe Blob do
end
end
+ describe '#data' do
+ context 'using a binary blob' do
+ it 'returns the data as-is' do
+ data = "\n\xFF\xB9\xC3"
+ blob = described_class.new(double(binary?: true, data: data))
+
+ expect(blob.data).to eq(data)
+ end
+ end
+
+ context 'using a text blob' do
+ it 'converts the data to UTF-8' do
+ blob = described_class.new(double(binary?: false, data: "\n\xFF\xB9\xC3"))
+
+ expect(blob.data).to eq("\n���")
+ end
+ end
+ end
+
describe '#svg?' do
it 'is falsey when not text' do
git_blob = double(text?: false)
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index c45c2635cf4..e7864b7ad33 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -88,9 +88,7 @@ describe Ci::Build, models: true do
end
describe '#trace' do
- subject { build.trace_html }
-
- it { is_expected.to be_empty }
+ it { expect(build.trace).to be_nil }
context 'when build.trace contains text' do
let(:text) { 'example output' }
@@ -98,16 +96,80 @@ describe Ci::Build, models: true do
build.trace = text
end
- it { is_expected.to include(text) }
- it { expect(subject.length).to be >= text.length }
+ it { expect(build.trace).to eq(text) }
+ end
+
+ context 'when build.trace hides runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(trace: token)
+ build.project.update(runners_token: token)
+ end
+
+ it { expect(build.trace).not_to include(token) }
+ it { expect(build.raw_trace).to include(token) }
+ end
+
+ context 'when build.trace hides build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(trace: token)
+ build.update(token: token)
+ end
+
+ it { expect(build.trace).not_to include(token) }
+ it { expect(build.raw_trace).to include(token) }
+ end
+ end
+
+ describe '#raw_trace' do
+ subject { build.raw_trace }
+
+ context 'when build.trace hides runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update(runners_token: token)
+ build.update(trace: token)
+ end
+
+ it { is_expected.not_to include(token) }
+ end
+
+ context 'when build.trace hides build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(token: token)
+ build.update(trace: token)
+ end
+
+ it { is_expected.not_to include(token) }
+ end
+ end
+
+ context '#append_trace' do
+ subject { build.trace_html }
+
+ context 'when build.trace hides runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update(runners_token: token)
+ build.append_trace(token, 0)
+ end
+
+ it { is_expected.not_to include(token) }
end
- context 'when build.trace hides token' do
+ context 'when build.trace hides build token' do
let(:token) { 'my_secret_token' }
before do
- build.project.update_attributes(runners_token: token)
- build.update_attributes(trace: token)
+ build.update(token: token)
+ build.append_trace(token, 0)
end
it { is_expected.not_to include(token) }
@@ -231,6 +293,34 @@ describe Ci::Build, models: true do
it { is_expected.to eq(predefined_variables) }
end
+ context 'when build has user' do
+ let(:user) { create(:user, username: 'starter') }
+ let(:user_variables) do
+ [
+ { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
+ { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }
+ ]
+ end
+
+ before do
+ build.update_attributes(user: user)
+ end
+
+ it { user_variables.each { |v| is_expected.to include(v) } }
+ end
+
+ context 'when build started manually' do
+ before do
+ build.update_attributes(when: :manual)
+ end
+
+ let(:manual_variable) do
+ { key: 'CI_BUILD_MANUAL', value: 'true', public: true }
+ end
+
+ it { is_expected.to include(manual_variable) }
+ end
+
context 'when build is for tag' do
let(:tag_variable) do
{ key: 'CI_BUILD_TAG', value: 'master', public: true }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 36d10636ae9..a37a00f461a 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -8,7 +8,7 @@ describe Ci::Build, models: true do
it 'obfuscates project runners token' do
allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}")
- expect(build.trace).to eq("Test: xxxxxx")
+ expect(build.trace).to eq("Test: xxxxxxxxxxxxxxxxxxxx")
end
it 'empty project runners token' do
@@ -19,4 +19,64 @@ describe Ci::Build, models: true do
expect(build.trace).to eq(test_trace)
end
end
+
+ describe '#has_trace_file?' do
+ context 'when there is no trace' do
+ it { expect(build.has_trace_file?).to be_falsey }
+ it { expect(build.trace).to be_nil }
+ end
+
+ context 'when there is a trace' do
+ context 'when trace is stored in file' do
+ let(:build_with_trace) { create(:ci_build, :trace) }
+
+ it { expect(build_with_trace.has_trace_file?).to be_truthy }
+ it { expect(build_with_trace.trace).to eq('BUILD TRACE') }
+ end
+
+ context 'when trace is stored in old file' do
+ before do
+ allow(build.project).to receive(:ci_id).and_return(999)
+ allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false)
+ allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(true)
+ allow(File).to receive(:read).with(build.old_path_to_trace).and_return(test_trace)
+ end
+
+ it { expect(build.has_trace_file?).to be_truthy }
+ it { expect(build.trace).to eq(test_trace) }
+ end
+
+ context 'when trace is stored in DB' do
+ before do
+ allow(build.project).to receive(:ci_id).and_return(nil)
+ allow(build).to receive(:read_attribute).with(:trace).and_return(test_trace)
+ allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false)
+ allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(false)
+ end
+
+ it { expect(build.has_trace_file?).to be_falsey }
+ it { expect(build.trace).to eq(test_trace) }
+ end
+ end
+ end
+
+ describe '#trace_file_path' do
+ context 'when trace is stored in file' do
+ before do
+ allow(build).to receive(:has_trace_file?).and_return(true)
+ allow(build).to receive(:has_old_trace_file?).and_return(false)
+ end
+
+ it { expect(build.trace_file_path).to eq(build.path_to_trace) }
+ end
+
+ context 'when trace is stored in old file' do
+ before do
+ allow(build).to receive(:has_trace_file?).and_return(true)
+ allow(build).to receive(:has_old_trace_file?).and_return(true)
+ end
+
+ it { expect(build.trace_file_path).to eq(build.old_path_to_trace) }
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 598df576001..550a890797e 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -124,21 +124,38 @@ describe Ci::Pipeline, models: true do
describe 'state machine' do
let(:current) { Time.now.change(usec: 0) }
- let(:build) { create :ci_build, name: 'build1', pipeline: pipeline }
+ let(:build) { create_build('build1', current, 10) }
+ let(:build_b) { create_build('build2', current, 20) }
+ let(:build_c) { create_build('build3', current + 50, 10) }
describe '#duration' do
before do
- travel_to(current - 120) do
+ pipeline.update(created_at: current)
+
+ travel_to(current + 5) do
pipeline.run
+ pipeline.save
+ end
+
+ travel_to(current + 30) do
+ build.success
+ end
+
+ travel_to(current + 40) do
+ build_b.drop
end
- travel_to(current) do
- pipeline.succeed
+ travel_to(current + 70) do
+ build_c.success
end
+
+ pipeline.drop
end
it 'matches sum of builds duration' do
- expect(pipeline.reload.duration).to eq(120)
+ pipeline.reload
+
+ expect(pipeline.duration).to eq(40)
end
end
@@ -169,6 +186,45 @@ describe Ci::Pipeline, models: true do
expect(pipeline.reload.finished_at).to be_nil
end
end
+
+ describe "merge request metrics" do
+ let(:project) { FactoryGirl.create :project }
+ let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
+
+ context 'when transitioning to running' do
+ it 'records the build start time' do
+ time = Time.now
+ Timecop.freeze(time) { build.run }
+
+ expect(merge_request.reload.metrics.latest_build_started_at).to be_within(1.second).of(time)
+ end
+
+ it 'clears the build end time' do
+ build.run
+
+ expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil
+ end
+ end
+
+ context 'when transitioning to success' do
+ it 'records the build end time' do
+ build.run
+ time = Time.now
+ Timecop.freeze(time) { build.success }
+
+ expect(merge_request.reload.metrics.latest_build_finished_at).to be_within(1.second).of(time)
+ end
+ end
+ end
+
+ def create_build(name, queued_at = current, started_from = 0)
+ create(:ci_build,
+ name: name,
+ pipeline: pipeline,
+ queued_at: queued_at,
+ started_at: queued_at + started_from)
+ end
end
describe '#branch?' do
@@ -348,8 +404,8 @@ describe Ci::Pipeline, models: true do
end
describe '#execute_hooks' do
- let!(:build_a) { create_build('a') }
- let!(:build_b) { create_build('b') }
+ let!(:build_a) { create_build('a', 0) }
+ let!(:build_b) { create_build('b', 1) }
let!(:hook) do
create(:project_hook, project: project, pipeline_events: enabled)
@@ -373,7 +429,7 @@ describe Ci::Pipeline, models: true do
build_b.enqueue
end
- it 'receive a pending event once' do
+ it 'receives a pending event once' do
expect(WebMock).to have_requested_pipeline_hook('pending').once
end
end
@@ -386,7 +442,7 @@ describe Ci::Pipeline, models: true do
build_b.run
end
- it 'receive a running event once' do
+ it 'receives a running event once' do
expect(WebMock).to have_requested_pipeline_hook('running').once
end
end
@@ -397,11 +453,21 @@ describe Ci::Pipeline, models: true do
build_b.success
end
- it 'receive a success event once' do
+ it 'receives a success event once' do
expect(WebMock).to have_requested_pipeline_hook('success').once
end
end
+ context 'when stage one failed' do
+ before do
+ build_a.drop
+ end
+
+ it 'receives a failed event once' do
+ expect(WebMock).to have_requested_pipeline_hook('failed').once
+ end
+ end
+
def have_requested_pipeline_hook(status)
have_requested(:post, hook.url).with do |req|
json_body = JSON.parse(req.body)
@@ -425,8 +491,36 @@ describe Ci::Pipeline, models: true do
end
end
- def create_build(name)
- create(:ci_build, :created, pipeline: pipeline, name: name)
+ def create_build(name, stage_idx)
+ create(:ci_build,
+ :created,
+ pipeline: pipeline,
+ name: name,
+ stage_idx: stage_idx)
+ end
+ end
+
+ describe "#merge_requests" do
+ let(:project) { FactoryGirl.create :project }
+ let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
+
+ it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do
+ merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+
+ expect(pipeline.merge_requests).to eq([merge_request])
+ end
+
+ it "doesn't return merge requests whose source branch doesn't match the pipeline's ref" do
+ create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master')
+
+ expect(pipeline.merge_requests).to be_empty
+ end
+
+ it "doesn't return merge requests whose `diff_head_sha` doesn't match the pipeline's SHA" do
+ create(:merge_request, source_project: project, source_branch: pipeline.ref)
+ allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { '97de212e80737a608d939f648d959671fb0a0142b' }
+
+ expect(pipeline.merge_requests).to be_empty
end
end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index fcfa3138ce5..2f1baff5d66 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -40,7 +40,7 @@ describe CommitStatus, models: true do
it { is_expected.to be_falsey }
end
- %w(running success failed).each do |status|
+ %w[running success failed].each do |status|
context "if commit status is #{status}" do
before { commit_status.status = status }
@@ -48,7 +48,7 @@ describe CommitStatus, models: true do
end
end
- %w(pending canceled).each do |status|
+ %w[pending canceled].each do |status|
context "if commit status is #{status}" do
before { commit_status.status = status }
@@ -60,7 +60,7 @@ describe CommitStatus, models: true do
describe '#active?' do
subject { commit_status.active? }
- %w(pending running).each do |state|
+ %w[pending running].each do |state|
context "if commit_status.status is #{state}" do
before { commit_status.status = state }
@@ -68,7 +68,7 @@ describe CommitStatus, models: true do
end
end
- %w(success failed canceled).each do |state|
+ %w[success failed canceled].each do |state|
context "if commit_status.status is #{state}" do
before { commit_status.status = state }
@@ -80,7 +80,7 @@ describe CommitStatus, models: true do
describe '#complete?' do
subject { commit_status.complete? }
- %w(success failed canceled).each do |state|
+ %w[success failed canceled].each do |state|
context "if commit_status.status is #{state}" do
before { commit_status.status = state }
@@ -88,7 +88,7 @@ describe CommitStatus, models: true do
end
end
- %w(pending running).each do |state|
+ %w[pending running].each do |state|
context "if commit_status.status is #{state}" do
before { commit_status.status = state }
@@ -187,7 +187,7 @@ describe CommitStatus, models: true do
subject { CommitStatus.where(pipeline: pipeline).stages }
it 'returns ordered list of stages' do
- is_expected.to eq(%w(build test deploy))
+ is_expected.to eq(%w[build test deploy])
end
end
@@ -223,4 +223,33 @@ describe CommitStatus, models: true do
expect(commit_status.commit).to eq project.commit
end
end
+
+ describe '#group_name' do
+ subject { commit_status.group_name }
+
+ tests = {
+ 'rspec:windows' => 'rspec:windows',
+ 'rspec:windows 0' => 'rspec:windows 0',
+ 'rspec:windows 0 test' => 'rspec:windows 0 test',
+ 'rspec:windows 0 1' => 'rspec:windows',
+ 'rspec:windows 0 1 name' => 'rspec:windows name',
+ 'rspec:windows 0/1' => 'rspec:windows',
+ 'rspec:windows 0/1 name' => 'rspec:windows name',
+ 'rspec:windows 0:1' => 'rspec:windows',
+ 'rspec:windows 0:1 name' => 'rspec:windows name',
+ 'rspec:windows 10000 20000' => 'rspec:windows',
+ 'rspec:windows 0 : / 1' => 'rspec:windows',
+ 'rspec:windows 0 : / 1 name' => 'rspec:windows name',
+ '0 1 name ruby' => 'name ruby',
+ '0 :/ 1 name ruby' => 'name ruby'
+ }
+
+ tests.each do |name, group_name|
+ it "'#{name}' puts in '#{group_name}'" do
+ commit_status.name = name
+
+ is_expected.to eq(group_name)
+ end
+ end
+ end
end
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
new file mode 100644
index 00000000000..b9381e33914
--- /dev/null
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#code', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :code,
+ data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
+ start_time_conditions: [["issue mentioned in a commit",
+ -> (context, data) do
+ context.create_commit_referencing_issue(data[:issue])
+ end]],
+ end_time_conditions: [["merge request that closes issue is created",
+ -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue])
+ end]],
+ post_fn: -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end)
+
+ context "when a regular merge request (that doesn't close the issue) is created" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+
+ create_commit_referencing_issue(issue)
+ create_merge_request_closing_issue(issue, message: "Closes nothing")
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.code).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
new file mode 100644
index 00000000000..e9cc71254ab
--- /dev/null
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#issue', models: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :issue,
+ data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
+ start_time_conditions: [["issue created", -> (context, data) { data[:issue].save }]],
+ end_time_conditions: [["issue associated with a milestone",
+ -> (context, data) do
+ if data[:issue].persisted?
+ data[:issue].update(milestone: context.create(:milestone, project: context.project))
+ end
+ end],
+ ["list label added to issue",
+ -> (context, data) do
+ if data[:issue].persisted?
+ data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id])
+ end
+ end]],
+ post_fn: -> (context, data) do
+ if data[:issue].persisted?
+ context.create_merge_request_closing_issue(data[:issue].reload)
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end
+ end)
+
+ context "when a regular label (instead of a list label) is added to the issue" do
+ it "returns nil" do
+ 5.times do
+ regular_label = create(:label)
+ issue = create(:issue, project: project)
+ issue.update(label_ids: [regular_label.id])
+
+ create_merge_request_closing_issue(issue)
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.issue).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
new file mode 100644
index 00000000000..5b8c96dc992
--- /dev/null
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#plan', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :plan,
+ data_fn: -> (context) do
+ {
+ issue: context.create(:issue, project: context.project),
+ branch_name: context.random_git_name
+ }
+ end,
+ start_time_conditions: [["issue associated with a milestone",
+ -> (context, data) do
+ data[:issue].update(milestone: context.create(:milestone, project: context.project))
+ end],
+ ["list label added to issue",
+ -> (context, data) do
+ data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id])
+ end]],
+ end_time_conditions: [["issue mentioned in a commit",
+ -> (context, data) do
+ context.create_commit_referencing_issue(data[:issue], branch_name: data[:branch_name])
+ end]],
+ post_fn: -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue], source_branch: data[:branch_name])
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end)
+
+ context "when a regular label (instead of a list label) is added to the issue" do
+ it "returns nil" do
+ branch_name = random_git_name
+ label = create(:label)
+ issue = create(:issue, project: project)
+ issue.update(label_ids: [label.id])
+ create_commit_referencing_issue(issue, branch_name: branch_name)
+
+ create_merge_request_closing_issue(issue, source_branch: branch_name)
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+
+ expect(subject.issue).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
new file mode 100644
index 00000000000..1f5e5cab92d
--- /dev/null
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#production', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :production,
+ data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
+ start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]],
+ before_end_fn: lambda do |context, data|
+ context.create_merge_request_closing_issue(data[:issue])
+ context.merge_merge_requests_closing_issue(data[:issue])
+ end,
+ end_time_conditions:
+ [["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master }],
+ ["production deploy happens after merge request is merged (along with other changes)",
+ lambda do |context, data|
+ # Make other changes on master
+ sha = context.project.repository.commit_file(context.user, context.random_git_name, "content", "commit message", 'master', false)
+ context.project.repository.commit(sha)
+
+ context.deploy_master
+ end]])
+
+ context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
+ it "returns nil" do
+ 5.times do
+ merge_request = create(:merge_request)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master
+ end
+
+ expect(subject.production).to be_nil
+ end
+ end
+
+ context "when the deployment happens to a non-production environment" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master(environment: 'staging')
+ end
+
+ expect(subject.production).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
new file mode 100644
index 00000000000..b6e26d8f261
--- /dev/null
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#review', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :review,
+ data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
+ start_time_conditions: [["merge request that closes issue is created",
+ -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue])
+ end]],
+ end_time_conditions: [["merge request that closes issue is merged",
+ -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ end]],
+ post_fn: -> (context, data) { context.deploy_master })
+
+ context "when a regular merge request (that doesn't close the issue) is created and merged" do
+ it "returns nil" do
+ 5.times do
+ MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
+
+ deploy_master
+ end
+
+ expect(subject.review).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
new file mode 100644
index 00000000000..af1c4477ddb
--- /dev/null
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#staging', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :staging,
+ data_fn: lambda do |context|
+ issue = context.create(:issue, project: context.project)
+ { issue: issue, merge_request: context.create_merge_request_closing_issue(issue) }
+ end,
+ start_time_conditions: [["merge request that closes issue is merged",
+ -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ end ]],
+ end_time_conditions: [["merge request that closes issue is deployed to production",
+ -> (context, data) do
+ context.deploy_master
+ end],
+ ["production deploy happens after merge request is merged (along with other changes)",
+ lambda do |context, data|
+ # Make other changes on master
+ sha = context.project.repository.commit_file(
+ context.user,
+ context.random_git_name,
+ "content",
+ "commit message",
+ 'master',
+ false)
+ context.project.repository.commit(sha)
+
+ context.deploy_master
+ end]])
+
+ context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
+ it "returns nil" do
+ 5.times do
+ merge_request = create(:merge_request)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master
+ end
+
+ expect(subject.staging).to be_nil
+ end
+ end
+
+ context "when the deployment happens to a non-production environment" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master(environment: 'staging')
+ end
+
+ expect(subject.staging).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb
new file mode 100644
index 00000000000..9d67bc82cba
--- /dev/null
+++ b/spec/models/cycle_analytics/summary_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe CycleAnalytics::Summary, models: true do
+ let(:project) { create(:project) }
+ let(:from) { Time.now }
+ let(:user) { create(:user, :admin) }
+ subject { described_class.new(project, from: from) }
+
+ describe "#new_issues" do
+ it "finds the number of issues created after the 'from date'" do
+ Timecop.freeze(5.days.ago) { create(:issue, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
+
+ expect(subject.new_issues).to eq(1)
+ end
+
+ it "doesn't find issues from other projects" do
+ Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) }
+
+ expect(subject.new_issues).to eq(0)
+ end
+ end
+
+ describe "#commits" do
+ it "finds the number of commits created after the 'from date'" do
+ Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
+ Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
+
+ expect(subject.commits).to eq(1)
+ end
+
+ it "doesn't find commits from other projects" do
+ Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') }
+
+ expect(subject.commits).to eq(0)
+ end
+
+ it "finds a large (> 100) snumber of commits if present" do
+ Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
+
+ expect(subject.commits).to eq(100)
+ end
+ end
+
+ describe "#deploys" do
+ it "finds the number of deploys made created after the 'from date'" do
+ Timecop.freeze(5.days.ago) { create(:deployment, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:deployment, project: project) }
+
+ expect(subject.deploys).to eq(1)
+ end
+
+ it "doesn't find commits from other projects" do
+ Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) }
+
+ expect(subject.deploys).to eq(0)
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
new file mode 100644
index 00000000000..89ace0b2742
--- /dev/null
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#test', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :test,
+ data_fn: lambda do |context|
+ issue = context.create(:issue, project: context.project)
+ merge_request = context.create_merge_request_closing_issue(issue)
+ pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project)
+ { pipeline: pipeline, issue: issue }
+ end,
+ start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]],
+ end_time_conditions: [["pipeline is finished", -> (context, data) { data[:pipeline].succeed! }]],
+ post_fn: -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end)
+
+ context "when the pipeline is for a regular merge request (that doesn't close an issue)" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+
+ pipeline.run!
+ pipeline.succeed!
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.test).to be_nil
+ end
+ end
+
+ context "when the pipeline is not for a merge request" do
+ it "returns nil" do
+ 5.times do
+ pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha)
+
+ pipeline.run!
+ pipeline.succeed!
+
+ deploy_master
+ end
+
+ expect(subject.test).to be_nil
+ end
+ end
+
+ context "when the pipeline is dropped (failed)" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+
+ pipeline.run!
+ pipeline.drop!
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.test).to be_nil
+ end
+ end
+
+ context "when the pipeline is cancelled" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+
+ pipeline.run!
+ pipeline.cancel!
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.test).to be_nil
+ end
+ end
+end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 6a640474cfe..3db5937a4f3 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -31,6 +31,43 @@ describe DiffNote, models: true do
subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
+ describe ".resolve!" do
+ let(:current_user) { create(:user) }
+ let!(:commit_note) { create(:diff_note_on_commit) }
+ let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
+ let!(:unresolved_note) { create(:diff_note_on_merge_request) }
+
+ before do
+ described_class.resolve!(current_user)
+
+ commit_note.reload
+ resolved_note.reload
+ unresolved_note.reload
+ end
+
+ it 'resolves only the resolvable, not yet resolved notes' do
+ expect(commit_note.resolved_at).to be_nil
+ expect(resolved_note.resolved_by).not_to eq(current_user)
+ expect(unresolved_note.resolved_at).not_to be_nil
+ expect(unresolved_note.resolved_by).to eq(current_user)
+ end
+ end
+
+ describe ".unresolve!" do
+ let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
+
+ before do
+ described_class.unresolve!
+
+ resolved_note.reload
+ end
+
+ it 'unresolves the resolved notes' do
+ expect(resolved_note.resolved_by).to be_nil
+ expect(resolved_note.resolved_at).to be_nil
+ end
+ end
+
describe "#position=" do
context "when provided a string" do
it "sets the position" do
diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb
index 179f2e73662..0142706d140 100644
--- a/spec/models/discussion_spec.rb
+++ b/spec/models/discussion_spec.rb
@@ -238,27 +238,19 @@ describe Discussion, model: true do
context "when resolvable" do
let(:user) { create(:user) }
+ let(:second_note) { create(:diff_note_on_commit) } # unresolvable
before do
allow(subject).to receive(:resolvable?).and_return(true)
-
- allow(first_note).to receive(:resolvable?).and_return(true)
- allow(second_note).to receive(:resolvable?).and_return(false)
- allow(third_note).to receive(:resolvable?).and_return(true)
end
context "when all resolvable notes are resolved" do
before do
first_note.resolve!(user)
third_note.resolve!(user)
- end
- it "calls resolve! on every resolvable note" do
- expect(first_note).to receive(:resolve!).with(current_user)
- expect(second_note).not_to receive(:resolve!)
- expect(third_note).to receive(:resolve!).with(current_user)
-
- subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
end
it "doesn't change resolved_at on the resolved notes" do
@@ -309,46 +301,44 @@ describe Discussion, model: true do
first_note.resolve!(user)
end
- it "calls resolve! on every resolvable note" do
- expect(first_note).to receive(:resolve!).with(current_user)
- expect(second_note).not_to receive(:resolve!)
- expect(third_note).to receive(:resolve!).with(current_user)
-
- subject.resolve!(current_user)
- end
-
it "doesn't change resolved_at on the resolved note" do
expect(first_note.resolved_at).not_to be_nil
- expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at }
+ expect { subject.resolve!(current_user) }.
+ not_to change { first_note.reload.resolved_at }
end
it "doesn't change resolved_by on the resolved note" do
expect(first_note.resolved_by).to eq(user)
- expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by }
+ expect { subject.resolve!(current_user) }.
+ not_to change { first_note.reload && first_note.resolved_by }
end
it "doesn't change the resolved state on the resolved note" do
expect(first_note.resolved?).to be true
- expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? }
+ expect { subject.resolve!(current_user) }.
+ not_to change { first_note.reload && first_note.resolved? }
end
it "sets resolved_at on the unresolved note" do
subject.resolve!(current_user)
+ third_note.reload
expect(third_note.resolved_at).not_to be_nil
end
it "sets resolved_by on the unresolved note" do
subject.resolve!(current_user)
+ third_note.reload
expect(third_note.resolved_by).to eq(current_user)
end
it "marks the unresolved note as resolved" do
subject.resolve!(current_user)
+ third_note.reload
expect(third_note.resolved?).to be true
end
@@ -373,16 +363,10 @@ describe Discussion, model: true do
end
context "when no resolvable notes are resolved" do
- it "calls resolve! on every resolvable note" do
- expect(first_note).to receive(:resolve!).with(current_user)
- expect(second_note).not_to receive(:resolve!)
- expect(third_note).to receive(:resolve!).with(current_user)
-
- subject.resolve!(current_user)
- end
-
it "sets resolved_at on the unresolved notes" do
subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
expect(first_note.resolved_at).not_to be_nil
expect(third_note.resolved_at).not_to be_nil
@@ -390,6 +374,8 @@ describe Discussion, model: true do
it "sets resolved_by on the unresolved notes" do
subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
expect(first_note.resolved_by).to eq(current_user)
expect(third_note.resolved_by).to eq(current_user)
@@ -397,6 +383,8 @@ describe Discussion, model: true do
it "marks the unresolved notes as resolved" do
subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
expect(first_note.resolved?).to be true
expect(third_note.resolved?).to be true
@@ -404,18 +392,24 @@ describe Discussion, model: true do
it "sets resolved_at" do
subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
expect(subject.resolved_at).not_to be_nil
end
it "sets resolved_by" do
subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
expect(subject.resolved_by).to eq(current_user)
end
it "marks as resolved" do
subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
expect(subject.resolved?).to be true
end
@@ -451,16 +445,10 @@ describe Discussion, model: true do
third_note.resolve!(user)
end
- it "calls unresolve! on every resolvable note" do
- expect(first_note).to receive(:unresolve!)
- expect(second_note).not_to receive(:unresolve!)
- expect(third_note).to receive(:unresolve!)
-
- subject.unresolve!
- end
-
it "unsets resolved_at on the resolved notes" do
subject.unresolve!
+ first_note.reload
+ third_note.reload
expect(first_note.resolved_at).to be_nil
expect(third_note.resolved_at).to be_nil
@@ -468,6 +456,8 @@ describe Discussion, model: true do
it "unsets resolved_by on the resolved notes" do
subject.unresolve!
+ first_note.reload
+ third_note.reload
expect(first_note.resolved_by).to be_nil
expect(third_note.resolved_by).to be_nil
@@ -475,6 +465,8 @@ describe Discussion, model: true do
it "unmarks the resolved notes as resolved" do
subject.unresolve!
+ first_note.reload
+ third_note.reload
expect(first_note.resolved?).to be false
expect(third_note.resolved?).to be false
@@ -482,12 +474,16 @@ describe Discussion, model: true do
it "unsets resolved_at" do
subject.unresolve!
+ first_note.reload
+ third_note.reload
expect(subject.resolved_at).to be_nil
end
it "unsets resolved_by" do
subject.unresolve!
+ first_note.reload
+ third_note.reload
expect(subject.resolved_by).to be_nil
end
@@ -504,40 +500,22 @@ describe Discussion, model: true do
first_note.resolve!(user)
end
- it "calls unresolve! on every resolvable note" do
- expect(first_note).to receive(:unresolve!)
- expect(second_note).not_to receive(:unresolve!)
- expect(third_note).to receive(:unresolve!)
-
- subject.unresolve!
- end
-
it "unsets resolved_at on the resolved note" do
subject.unresolve!
- expect(first_note.resolved_at).to be_nil
+ expect(subject.first_note.resolved_at).to be_nil
end
it "unsets resolved_by on the resolved note" do
subject.unresolve!
- expect(first_note.resolved_by).to be_nil
+ expect(subject.first_note.resolved_by).to be_nil
end
it "unmarks the resolved note as resolved" do
subject.unresolve!
- expect(first_note.resolved?).to be false
- end
- end
-
- context "when no resolvable notes are resolved" do
- it "calls unresolve! on every resolvable note" do
- expect(first_note).to receive(:unresolve!)
- expect(second_note).not_to receive(:unresolve!)
- expect(third_note).to receive(:unresolve!)
-
- subject.unresolve!
+ expect(subject.first_note.resolved?).to be false
end
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index c881897926e..6b1867a44e1 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -63,4 +63,20 @@ describe Environment, models: true do
end
end
end
+
+ describe '#environment_type' do
+ subject { environment.environment_type }
+
+ it 'sets a environment type if name has multiple segments' do
+ environment.update!(name: 'production/worker.gitlab.com')
+
+ is_expected.to eq('production')
+ end
+
+ it 'nullifies a type if it\'s a simple name' do
+ environment.update!(name: 'production')
+
+ is_expected.to be_nil
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index b5d0d79e14e..8600eb4d2c4 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -16,18 +16,12 @@ describe Event, models: true do
describe 'Callbacks' do
describe 'after_create :reset_project_activity' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
- context "project's last activity was less than 5 minutes ago" do
- it 'does not update project.last_activity_at if it has been touched less than 5 minutes ago' do
- create_event(project, project.owner)
- project.update_column(:last_activity_at, 5.minutes.ago)
- project_last_activity_at = project.last_activity_at
+ it 'calls the reset_project_activity method' do
+ expect_any_instance_of(Event).to receive(:reset_project_activity)
- create_event(project, project.owner)
-
- expect(project.last_activity_at).to eq(project_last_activity_at)
- end
+ create_event(project, project.owner)
end
end
end
@@ -161,6 +155,35 @@ describe Event, models: true do
end
end
+ describe '#reset_project_activity' do
+ let(:project) { create(:empty_project) }
+
+ context 'when a project was updated less than 1 hour ago' do
+ it 'does not update the project' do
+ project.update(last_activity_at: Time.now)
+
+ expect(project).not_to receive(:update_column).
+ with(:last_activity_at, a_kind_of(Time))
+
+ create_event(project, project.owner)
+ end
+ end
+
+ context 'when a project was updated more than 1 hour ago' do
+ it 'updates the project' do
+ project.update(last_activity_at: 1.year.ago)
+
+ expect_any_instance_of(Gitlab::ExclusiveLease).
+ to receive(:try_obtain).and_return(true)
+
+ expect(project).to receive(:update_column).
+ with(:last_activity_at, a_kind_of(Time))
+
+ create_event(project, project.owner)
+ end
+ end
+ end
+
def create_event(project, user, attrs = {})
data = {
before: Gitlab::Git::BLANK_SHA,
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index 92e0f7f27ce..dd033480527 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -50,8 +50,9 @@ describe GlobalMilestone, models: true do
milestone1_project2,
milestone1_project3,
]
+ milestones_relation = Milestone.where(id: milestones.map(&:id))
- @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones)
+ @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones_relation)
end
it 'has exactly one group milestone' do
@@ -67,7 +68,7 @@ describe GlobalMilestone, models: true do
let(:milestone) { create(:milestone, title: "git / test", project: project1) }
it 'strips out slashes and spaces' do
- global_milestone = GlobalMilestone.new(milestone.title, [milestone])
+ global_milestone = GlobalMilestone.new(milestone.title, Milestone.where(id: milestone.id))
expect(global_milestone.safe_title).to eq('git-test')
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index ea4b59c26b1..0b3ef9b98fd 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -187,6 +187,52 @@ describe Group, models: true do
it { expect(group.has_master?(@members[:requester])).to be_falsey }
end
+ describe '#lfs_enabled?' do
+ context 'LFS enabled globally' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ it 'returns true when nothing is set' do
+ expect(group.lfs_enabled?).to be_truthy
+ end
+
+ it 'returns false when set to false' do
+ group.update_attribute(:lfs_enabled, false)
+
+ expect(group.lfs_enabled?).to be_falsey
+ end
+
+ it 'returns true when set to true' do
+ group.update_attribute(:lfs_enabled, true)
+
+ expect(group.lfs_enabled?).to be_truthy
+ end
+ end
+
+ context 'LFS disabled globally' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
+ end
+
+ it 'returns false when nothing is set' do
+ expect(group.lfs_enabled?).to be_falsey
+ end
+
+ it 'returns false when set to false' do
+ group.update_attribute(:lfs_enabled, false)
+
+ expect(group.lfs_enabled?).to be_falsey
+ end
+
+ it 'returns false when set to true' do
+ group.update_attribute(:lfs_enabled, true)
+
+ expect(group.lfs_enabled?).to be_falsey
+ end
+ end
+ end
+
describe '#owners' do
let(:owner) { create(:user) }
let(:developer) { create(:user) }
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 4a457997a4f..474ae62ccec 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# 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
-#
-
require 'spec_helper'
describe ProjectHook, models: true do
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 534e1b4f128..1a83c836652 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# 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
-#
-
require "spec_helper"
describe ServiceHook, models: true do
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index cbdf7eec082..ad2b710041a 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# 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
-#
-
require "spec_helper"
describe SystemHook, models: true do
@@ -48,7 +30,7 @@ describe SystemHook, models: true do
it "user_create hook" do
create(:user)
-
+
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_create/,
headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index f9bab487b96..e52b9d75cef 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -1,21 +1,3 @@
-# == Schema Information
-#
-# 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
-#
-
require 'spec_helper'
describe WebHook, models: true do
diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb
new file mode 100644
index 00000000000..e170b087ebc
--- /dev/null
+++ b/spec/models/issue/metrics_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Issue::Metrics, models: true do
+ let(:project) { create(:project) }
+
+ subject { create(:issue, project: project) }
+
+ describe "when recording the default set of issue metrics on issue save" do
+ context "milestones" do
+ it "records the first time an issue is associated with a milestone" do
+ time = Time.now
+ Timecop.freeze(time) { subject.update(milestone: create(:milestone)) }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.first_associated_with_milestone_at).to be_within(1.second).of(time)
+ end
+
+ it "does not record the second time an issue is associated with a milestone" do
+ time = Time.now
+ Timecop.freeze(time) { subject.update(milestone: create(:milestone)) }
+ Timecop.freeze(time + 2.hours) { subject.update(milestone: nil) }
+ Timecop.freeze(time + 6.hours) { subject.update(milestone: create(:milestone)) }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.first_associated_with_milestone_at).to be_within(1.second).of(time)
+ end
+ end
+
+ context "list labels" do
+ it "records the first time an issue is associated with a list label" do
+ list_label = create(:label, lists: [create(:list)])
+ time = Time.now
+ Timecop.freeze(time) { subject.update(label_ids: [list_label.id]) }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.first_added_to_board_at).to be_within(1.second).of(time)
+ end
+
+ it "does not record the second time an issue is associated with a list label" do
+ time = Time.now
+ first_list_label = create(:label, lists: [create(:list)])
+ Timecop.freeze(time) { subject.update(label_ids: [first_list_label.id]) }
+ second_list_label = create(:label, lists: [create(:list)])
+ Timecop.freeze(time + 5.hours) { subject.update(label_ids: [second_list_label.id]) }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.first_added_to_board_at).to be_within(1.second).of(time)
+ end
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 3259f795296..3b8b743af2d 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -494,7 +494,7 @@ describe Issue, models: true do
context 'with an admin user' do
let(:project) { create(:empty_project) }
- let(:user) { create(:user, admin: true) }
+ let(:user) { create(:admin) }
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index fef90d9b5cb..bda23eaed43 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -57,7 +57,7 @@ describe Member, models: true do
describe 'Scopes & finders' do
before do
- project = create(:project)
+ project = create(:empty_project, :public)
group = create(:group)
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@owner = group.members.find_by(user_id: @owner_user.id)
@@ -65,22 +65,26 @@ describe Member, models: true do
@master_user = create(:user).tap { |u| project.team << [u, :master] }
@master = project.members.find_by(user_id: @master_user.id)
- Member.add_user(
- project.members,
- 'toto1@example.com',
- Gitlab::Access::DEVELOPER,
- current_user: @master_user
- )
- @invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
-
- accepted_invite_user = build(:user)
- Member.add_user(
- project.members,
- 'toto2@example.com',
- Gitlab::Access::DEVELOPER,
- current_user: @master_user
- )
- @accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
+ @blocked_user = create(:user).tap do |u|
+ project.team << [u, :master]
+ project.team << [u, :developer]
+
+ u.block!
+ end
+ @blocked_master = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::MASTER)
+ @blocked_developer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::DEVELOPER)
+
+ @invited_member = create(:project_member, :developer,
+ project: project,
+ invite_token: '1234',
+ invite_email: 'toto1@example.com')
+
+ accepted_invite_user = build(:user, state: :active)
+ @accepted_invite_member = create(:project_member, :developer,
+ project: project,
+ invite_token: '1234',
+ invite_email: 'toto2@example.com').
+ tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) }
@requested_member = project.requesters.find_by(user_id: requested_user.id)
@@ -91,7 +95,7 @@ describe Member, models: true do
describe '.access_for_user_ids' do
it 'returns the right access levels' do
- users = [@owner_user.id, @master_user.id]
+ users = [@owner_user.id, @master_user.id, @blocked_user.id]
expected = {
@owner_user.id => Gitlab::Access::OWNER,
@master_user.id => Gitlab::Access::MASTER
@@ -125,6 +129,19 @@ describe Member, models: true do
it { expect(described_class.request).not_to include @accepted_request_member }
end
+ describe '.developers' do
+ subject { described_class.developers.to_a }
+
+ it { is_expected.not_to include @owner }
+ it { is_expected.not_to include @master }
+ it { is_expected.to include @invited_member }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.not_to include @requested_member }
+ it { is_expected.to include @accepted_request_member }
+ it { is_expected.not_to include @blocked_master }
+ it { is_expected.not_to include @blocked_developer }
+ end
+
describe '.owners_and_masters' do
it { expect(described_class.owners_and_masters).to include @owner }
it { expect(described_class.owners_and_masters).to include @master }
@@ -132,6 +149,20 @@ describe Member, models: true do
it { expect(described_class.owners_and_masters).not_to include @accepted_invite_member }
it { expect(described_class.owners_and_masters).not_to include @requested_member }
it { expect(described_class.owners_and_masters).not_to include @accepted_request_member }
+ it { expect(described_class.owners_and_masters).not_to include @blocked_master }
+ end
+
+ describe '.has_access' do
+ subject { described_class.has_access.to_a }
+
+ it { is_expected.to include @owner }
+ it { is_expected.to include @master }
+ it { is_expected.to include @invited_member }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.not_to include @requested_member }
+ it { is_expected.to include @accepted_request_member }
+ it { is_expected.not_to include @blocked_master }
+ it { is_expected.not_to include @blocked_developer }
end
end
@@ -140,39 +171,209 @@ describe Member, models: true do
it { is_expected.to respond_to(:user_email) }
end
- describe ".add_user" do
- let!(:user) { create(:user) }
- let(:project) { create(:project) }
+ describe '.add_user' do
+ %w[project group].each do |source_type|
+ context "when source is a #{source_type}" do
+ let!(:source) { create(source_type) }
+ let!(:user) { create(:user) }
+ let!(:admin) { create(:admin) }
- context "when called with a user id" do
- it "adds the user as a member" do
- Member.add_user(project.project_members, user.id, ProjectMember::MASTER)
+ it 'returns a <Source>Member object' do
+ member = described_class.add_user(source, user, :master)
- expect(project.users).to include(user)
- end
- end
+ expect(member).to be_a "#{source_type.classify}Member".constantize
+ expect(member).to be_persisted
+ end
- context "when called with a user object" do
- it "adds the user as a member" do
- Member.add_user(project.project_members, user, ProjectMember::MASTER)
+ it 'sets members.created_by to the given current_user' do
+ member = described_class.add_user(source, user, :master, current_user: admin)
- expect(project.users).to include(user)
- end
- end
+ expect(member.created_by).to eq(admin)
+ end
- context "when called with a known user email" do
- it "adds the user as a member" do
- Member.add_user(project.project_members, user.email, ProjectMember::MASTER)
+ it 'sets members.expires_at to the given expires_at' do
+ member = described_class.add_user(source, user, :master, expires_at: Date.new(2016, 9, 22))
- expect(project.users).to include(user)
- end
- end
+ expect(member.expires_at).to eq(Date.new(2016, 9, 22))
+ end
+
+ described_class.access_levels.each do |sym_key, int_access_level|
+ it "accepts the :#{sym_key} symbol as access level" do
+ expect(source.users).not_to include(user)
+
+ member = described_class.add_user(source, user.id, sym_key)
+
+ expect(member.access_level).to eq(int_access_level)
+ expect(source.users.reload).to include(user)
+ end
+
+ it "accepts the #{int_access_level} integer as access level" do
+ expect(source.users).not_to include(user)
+
+ member = described_class.add_user(source, user.id, int_access_level)
+
+ expect(member.access_level).to eq(int_access_level)
+ expect(source.users.reload).to include(user)
+ end
+ end
+
+ context 'with no current_user' do
+ context 'when called with a known user id' do
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, user.id, :master)
+
+ expect(source.users.reload).to include(user)
+ end
+ end
+
+ context 'when called with an unknown user id' do
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, 42, :master)
+
+ expect(source.users.reload).not_to include(user)
+ end
+ end
+
+ context 'when called with a user object' do
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, user, :master)
+
+ expect(source.users.reload).to include(user)
+ end
+ end
+
+ context 'when called with a requester user object' do
+ before do
+ source.request_access(user)
+ end
+
+ it 'adds the requester as a member' do
+ expect(source.users).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
+
+ expect { described_class.add_user(source, user, :master) }.
+ to raise_error(Gitlab::Access::AccessDeniedError)
+
+ expect(source.users.reload).not_to include(user)
+ expect(source.requesters.reload.exists?(user_id: user)).to be_truthy
+ end
+ end
+
+ context 'when called with a known user email' do
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, user.email, :master)
+
+ expect(source.users.reload).to include(user)
+ end
+ end
+
+ context 'when called with an unknown user email' do
+ it 'creates an invited member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, 'user@example.com', :master)
+
+ expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
+ end
+ end
+ end
+
+ context 'when current_user can update member' do
+ it 'creates the member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, user, :master, current_user: admin)
+
+ expect(source.users.reload).to include(user)
+ end
+
+ context 'when called with a requester user object' do
+ before do
+ source.request_access(user)
+ end
+
+ it 'adds the requester as a member' do
+ expect(source.users).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
+
+ described_class.add_user(source, user, :master, current_user: admin)
+
+ expect(source.users.reload).to include(user)
+ expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
+ end
+ end
+ end
+
+ context 'when current_user cannot update member' do
+ it 'does not create the member' do
+ expect(source.users).not_to include(user)
+
+ member = described_class.add_user(source, user, :master, current_user: user)
+
+ expect(source.users.reload).not_to include(user)
+ expect(member).not_to be_persisted
+ end
+
+ context 'when called with a requester user object' do
+ before do
+ source.request_access(user)
+ end
+
+ it 'does not destroy the requester' do
+ expect(source.users).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
+
+ described_class.add_user(source, user, :master, current_user: user)
+
+ expect(source.users.reload).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
+ end
+ end
+ end
+
+ context 'when member already exists' do
+ before do
+ source.add_user(user, :developer)
+ end
+
+ context 'with no current_user' do
+ it 'updates the member' do
+ expect(source.users).to include(user)
+
+ described_class.add_user(source, user, :master)
+
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MASTER)
+ end
+ end
+
+ context 'when current_user can update member' do
+ it 'updates the member' do
+ expect(source.users).to include(user)
+
+ described_class.add_user(source, user, :master, current_user: admin)
+
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MASTER)
+ end
+ end
+
+ context 'when current_user cannot update member' do
+ it 'does not update the member' do
+ expect(source.users).to include(user)
- context "when called with an unknown user email" do
- it "adds a member invite" do
- Member.add_user(project.project_members, "user@example.com", ProjectMember::MASTER)
+ described_class.add_user(source, user, :master, current_user: user)
- expect(project.project_members.invite.pluck(:invite_email)).to include("user@example.com")
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
+ end
+ end
+ end
end
end
end
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 4f875fd257a..370aeb9e0a9 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -1,25 +1,33 @@
-# == Schema Information
-#
-# Table name: members
-#
-# id :integer not null, primary key
-# access_level :integer not null
-# source_id :integer not null
-# source_type :string(255) not null
-# user_id :integer
-# notification_level :integer not null
-# type :string(255)
-# created_at :datetime
-# updated_at :datetime
-# created_by_id :integer
-# invite_email :string(255)
-# invite_token :string(255)
-# invite_accepted_at :datetime
-#
-
require 'spec_helper'
describe GroupMember, models: true do
+ describe '.access_level_roles' do
+ it 'returns Gitlab::Access.options_with_owner' do
+ expect(described_class.access_level_roles).to eq(Gitlab::Access.options_with_owner)
+ end
+ end
+
+ describe '.access_levels' do
+ it 'returns Gitlab::Access.options_with_owner' do
+ expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
+ end
+ end
+
+ describe '.add_users_to_group' do
+ it 'adds the given users to the given group' do
+ group = create(:group)
+ users = create_list(:user, 2)
+
+ described_class.add_users_to_group(
+ group,
+ [users.first.id, users.second],
+ described_class::MASTER
+ )
+
+ expect(group.users).to include(users.first, users.second)
+ end
+ end
+
describe 'notifications' do
describe "#after_create" do
it "sends email to user" do
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index be57957b569..d85a1c1e3b2 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -1,22 +1,3 @@
-# == Schema Information
-#
-# Table name: members
-#
-# id :integer not null, primary key
-# access_level :integer not null
-# source_id :integer not null
-# source_type :string(255) not null
-# user_id :integer
-# notification_level :integer not null
-# type :string(255)
-# created_at :datetime
-# updated_at :datetime
-# created_by_id :integer
-# invite_email :string(255)
-# invite_token :string(255)
-# invite_accepted_at :datetime
-#
-
require 'spec_helper'
describe ProjectMember, models: true do
@@ -34,6 +15,26 @@ describe ProjectMember, models: true do
it { is_expected.to include_module(Gitlab::ShellAdapter) }
end
+ describe '.access_level_roles' do
+ it 'returns Gitlab::Access.options' do
+ expect(described_class.access_level_roles).to eq(Gitlab::Access.options)
+ end
+ end
+
+ describe '.add_user' do
+ context 'when called with the project owner' do
+ it 'adds the user as a member' do
+ project = create(:empty_project)
+
+ expect(project.users).not_to include(project.owner)
+
+ described_class.add_user(project, project.owner, :master, current_user: project.owner)
+
+ expect(project.users.reload).to include(project.owner)
+ end
+ end
+ end
+
describe '#real_source_type' do
subject { create(:project_member).real_source_type }
@@ -69,7 +70,7 @@ describe ProjectMember, models: true do
end
end
- describe :import_team do
+ describe '.import_team' do
before do
@project_1 = create :project
@project_2 = create :project
@@ -100,25 +101,21 @@ describe ProjectMember, models: true do
end
describe '.add_users_to_projects' do
- before do
- @project_1 = create :project
- @project_2 = create :project
+ it 'adds the given users to the given projects' do
+ projects = create_list(:empty_project, 2)
+ users = create_list(:user, 2)
- @user_1 = create :user
- @user_2 = create :user
-
- ProjectMember.add_users_to_projects(
- [@project_1.id, @project_2.id],
- [@user_1.id, @user_2.id],
- ProjectMember::MASTER
- )
- end
+ described_class.add_users_to_projects(
+ [projects.first.id, projects.second],
+ [users.first.id, users.second],
+ described_class::MASTER)
- it { expect(@project_1.users).to include(@user_1) }
- it { expect(@project_1.users).to include(@user_2) }
+ expect(projects.first.users).to include(users.first)
+ expect(projects.first.users).to include(users.second)
- it { expect(@project_2.users).to include(@user_1) }
- it { expect(@project_2.users).to include(@user_2) }
+ expect(projects.second.users).to include(users.first)
+ expect(projects.second.users).to include(users.second)
+ end
end
describe '.truncate_teams' do
diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb
new file mode 100644
index 00000000000..a79dd215d41
--- /dev/null
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe MergeRequest::Metrics, models: true do
+ let(:project) { create(:project) }
+
+ subject { create(:merge_request, source_project: project) }
+
+ describe "when recording the default set of metrics on merge request save" do
+ it "records the merge time" do
+ time = Time.now
+ Timecop.freeze(time) { subject.mark_as_merged }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.merged_at).to be_within(1.second).of(time)
+ end
+ end
+end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index e5b185dc3f6..530a7def553 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -64,5 +64,27 @@ describe MergeRequestDiff, models: true do
end
end
end
+
+ describe '#commits_sha' do
+ shared_examples 'returning all commits SHA' do
+ it 'returns all commits SHA' do
+ commits_sha = subject.commits_sha
+
+ expect(commits_sha).to eq(subject.commits.map(&:sha))
+ end
+ end
+
+ context 'when commits were loaded' do
+ before do
+ subject.commits
+ end
+
+ it_behaves_like 'returning all commits SHA'
+ end
+
+ context 'when commits were not loaded' do
+ it_behaves_like 'returning all commits SHA'
+ end
+ end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 5bf3b8e609e..580a3235127 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -328,6 +328,42 @@ describe MergeRequest, models: true do
end
end
+ describe '#merge_commit_message' do
+ it 'includes merge information as the title' do
+ request = build(:merge_request, source_branch: 'source', target_branch: 'target')
+
+ expect(request.merge_commit_message)
+ .to match("Merge branch 'source' into 'target'\n\n")
+ end
+
+ it 'includes its title in the body' do
+ request = build(:merge_request, title: 'Remove all technical debt')
+
+ expect(request.merge_commit_message)
+ .to match("Remove all technical debt\n\n")
+ end
+
+ it 'includes its description in the body' do
+ request = build(:merge_request, description: 'By removing all code')
+
+ expect(request.merge_commit_message)
+ .to match("By removing all code\n\n")
+ end
+
+ it 'includes its reference in the body' do
+ request = build_stubbed(:merge_request)
+
+ expect(request.merge_commit_message)
+ .to match("See merge request #{request.to_reference}")
+ end
+
+ it 'excludes multiple linebreak runs when description is blank' do
+ request = build(:merge_request, title: 'Title', description: nil)
+
+ expect(request.merge_commit_message).not_to match("Title\n\n\n\n")
+ end
+ end
+
describe "#reset_merge_when_build_succeeds" do
let(:merge_if_green) do
create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user),
@@ -495,15 +531,77 @@ describe MergeRequest, models: true do
end
describe '#all_pipelines' do
- let!(:pipelines) do
- subject.merge_request_diff.commits.map do |commit|
- create(:ci_empty_pipeline, project: subject.source_project, sha: commit.id, ref: subject.source_branch)
+ shared_examples 'returning pipelines with proper ordering' do
+ let!(:all_pipelines) do
+ subject.all_commits_sha.map do |sha|
+ create(:ci_empty_pipeline,
+ project: subject.source_project,
+ sha: sha,
+ ref: subject.source_branch)
+ end
+ end
+
+ it 'returns all pipelines' do
+ expect(subject.all_pipelines).not_to be_empty
+ expect(subject.all_pipelines).to eq(all_pipelines.reverse)
+ end
+ end
+
+ context 'with single merge_request_diffs' do
+ it_behaves_like 'returning pipelines with proper ordering'
+ end
+
+ context 'with multiple irrelevant merge_request_diffs' do
+ before do
+ subject.update(target_branch: 'v1.0.0')
+ end
+
+ it_behaves_like 'returning pipelines with proper ordering'
+ end
+
+ context 'with unsaved merge request' do
+ subject { build(:merge_request) }
+
+ let!(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: subject.project,
+ sha: subject.diff_head_sha,
+ ref: subject.source_branch)
+ end
+
+ it 'returns pipelines from diff_head_sha' do
+ expect(subject.all_pipelines).to contain_exactly(pipeline)
+ end
+ end
+ end
+
+ describe '#all_commits_sha' do
+ let(:all_commits_sha) do
+ subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
+ end
+
+ shared_examples 'returning all SHA' do
+ it 'returns all SHA from all merge_request_diffs' do
+ expect(subject.merge_request_diffs.size).to eq(2)
+ expect(subject.all_commits_sha).to eq(all_commits_sha)
end
end
- it 'returns a pipelines from source projects with proper ordering' do
- expect(subject.all_pipelines).not_to be_empty
- expect(subject.all_pipelines).to eq(pipelines.reverse)
+ context 'with a completely different branch' do
+ before do
+ subject.update(target_branch: 'v1.0.0')
+ end
+
+ it_behaves_like 'returning all SHA'
+ end
+
+ context 'with a branch having no difference' do
+ before do
+ subject.update(target_branch: 'v1.1.0')
+ subject.reload # make sure commits were not cached
+ end
+
+ it_behaves_like 'returning all SHA'
end
end
@@ -701,18 +799,67 @@ describe MergeRequest, models: true do
end
end
- describe "#environments" do
+ describe '#environments' do
let(:project) { create(:project) }
- let!(:environment) { create(:environment, project: project) }
- let!(:environment1) { create(:environment, project: project) }
- let!(:environment2) { create(:environment, project: project) }
let(:merge_request) { create(:merge_request, source_project: project) }
- it 'selects deployed environments' do
- create(:deployment, environment: environment, sha: project.commit('master').id)
- create(:deployment, environment: environment1, sha: project.commit('feature').id)
+ context 'with multiple environments' do
+ let(:environments) { create_list(:environment, 3, project: project) }
+
+ before do
+ create(:deployment, environment: environments.first, ref: 'master', sha: project.commit('master').id)
+ create(:deployment, environment: environments.second, ref: 'feature', sha: project.commit('feature').id)
+ end
- expect(merge_request.environments).to eq [environment]
+ it 'selects deployed environments' do
+ expect(merge_request.environments).to contain_exactly(environments.first)
+ end
+ end
+
+ context 'with environments on source project' do
+ let(:source_project) do
+ create(:project) do |fork_project|
+ fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
+ end
+ end
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: source_project, source_branch: 'feature',
+ target_project: project)
+ end
+
+ let(:source_environment) { create(:environment, project: source_project) }
+
+ before do
+ create(:deployment, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha)
+ end
+
+ it 'selects deployed environments' do
+ expect(merge_request.environments).to contain_exactly(source_environment)
+ end
+
+ context 'with environments on target project' do
+ let(:target_environment) { create(:environment, project: project) }
+
+ before do
+ create(:deployment, environment: target_environment, tag: true, sha: merge_request.diff_head_sha)
+ end
+
+ it 'selects deployed environments' do
+ expect(merge_request.environments).to contain_exactly(source_environment, target_environment)
+ end
+ end
+ end
+
+ context 'without a diff_head_commit' do
+ before do
+ expect(merge_request).to receive(:diff_head_commit).and_return(nil)
+ end
+
+ it 'returns an empty array' do
+ expect(merge_request.environments).to be_empty
+ end
end
end
@@ -1038,4 +1185,81 @@ describe MergeRequest, models: true do
end
end
end
+
+ describe '#closed_without_source_project?' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
+ let(:destroy_service) { Projects::DestroyService.new(fork_project, user) }
+
+ context 'when the merge request is closed' do
+ let(:closed_merge_request) do
+ create(:closed_merge_request,
+ source_project: fork_project,
+ target_project: project)
+ end
+
+ it 'returns false if the source project exists' do
+ expect(closed_merge_request.closed_without_source_project?).to be_falsey
+ end
+
+ it 'returns true if the source project does not exist' do
+ destroy_service.execute
+ closed_merge_request.reload
+
+ expect(closed_merge_request.closed_without_source_project?).to be_truthy
+ end
+ end
+
+ context 'when the merge request is open' do
+ it 'returns false' do
+ expect(subject.closed_without_source_project?).to be_falsey
+ end
+ end
+ end
+
+ describe '#reopenable?' do
+ context 'when the merge request is closed' do
+ it 'returns true' do
+ subject.close
+
+ expect(subject.reopenable?).to be_truthy
+ end
+
+ context 'forked project' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
+ let(:merge_request) do
+ create(:closed_merge_request,
+ source_project: fork_project,
+ target_project: project)
+ end
+
+ it 'returns false if unforked' do
+ Projects::UnlinkForkService.new(fork_project, user).execute
+
+ expect(merge_request.reload.reopenable?).to be_falsey
+ end
+
+ it 'returns false if the source project is deleted' do
+ Projects::DestroyService.new(fork_project, user).execute
+
+ expect(merge_request.reload.reopenable?).to be_falsey
+ end
+
+ it 'returns false if the merge request is merged' do
+ merge_request.update_attributes(state: 'merged')
+
+ expect(merge_request.reload.reopenable?).to be_falsey
+ end
+ end
+ end
+
+ context 'when the merge request is opened' do
+ it 'returns false' do
+ expect(subject.reopenable?).to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index d64d6cde2b5..33fe22dd98c 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -20,10 +20,10 @@ describe Milestone, models: true do
let(:user) { create(:user) }
describe "#title" do
- let(:milestone) { create(:milestone, title: "<b>test</b>") }
+ let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") }
it "sanitizes title" do
- expect(milestone.title).to eq("test")
+ expect(milestone.title).to eq("foo & bar -> 2.2")
end
end
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index dc702cfc42c..8e5145e824b 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 AsanaService, models: true do
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index d672d80156c..4c5acb7990b 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 AssemblaService, models: true do
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index 9ae461f8c2d..d7e1a4e3b6c 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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
diff --git a/spec/models/project_services/bugzilla_service_spec.rb b/spec/models/project_services/bugzilla_service_spec.rb
index a6d9717ccb5..739cc72b2ff 100644
--- a/spec/models/project_services/bugzilla_service_spec.rb
+++ b/spec/models/project_services/bugzilla_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 BugzillaService, models: true do
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 0866e1532dd..6f65beb79d0 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 BuildkiteService, models: true do
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index c76ae21421b..a3b9d084a75 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 CampfireService, models: true do
diff --git a/spec/models/project_services/custom_issue_tracker_service_spec.rb b/spec/models/project_services/custom_issue_tracker_service_spec.rb
index ff976f8ec59..63320931e76 100644
--- a/spec/models/project_services/custom_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/custom_issue_tracker_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 CustomIssueTrackerService, models: true do
@@ -45,5 +25,21 @@ describe CustomIssueTrackerService, models: true do
it { is_expected.not_to validate_presence_of(:issues_url) }
it { is_expected.not_to validate_presence_of(:new_issue_url) }
end
+
+ context 'title' do
+ let(:issue_tracker) { described_class.new(properties: {}) }
+
+ it 'sets a default title' do
+ issue_tracker.title = nil
+
+ expect(issue_tracker.title).to eq('Custom Issue Tracker')
+ end
+
+ it 'sets the custom title' do
+ issue_tracker.title = 'test title'
+
+ expect(issue_tracker.title).to eq('test title')
+ end
+ end
end
end
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index 8ef892259f2..f13bb1e8adf 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 DroneCiService, models: true do
diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb
index d7c5ea95d71..342d86aeca9 100644
--- a/spec/models/project_services/external_wiki_service_spec.rb
+++ b/spec/models/project_services/external_wiki_service_spec.rb
@@ -1,24 +1,3 @@
-# == 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
-# build_events :boolean default(FALSE), not null
-#
-
require 'spec_helper'
describe ExternalWikiService, models: true do
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index d2557019756..d6db02d6e76 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 FlowdockService, models: true do
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index 3d0b6c9816b..529044d1d8b 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 GemnasiumService, models: true do
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index 8ef79a17d50..652804fb444 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 GitlabIssueTrackerService, models: true do
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 34eafbe555d..cf713684463 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 HipchatService, models: true do
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index ffb17fd3259..f8c45b37561 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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'
require 'socket'
require 'json'
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 9037ca5cc20..b48a3176007 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 JiraService, models: true do
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index d098d988521..45b2f1068bf 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 PivotaltrackerService, models: true do
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index 5959c81577d..8fc92a9ab51 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 PushoverService, models: true do
diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb
index 7d14f6e8280..b8679cd2563 100644
--- a/spec/models/project_services/redmine_service_spec.rb
+++ b/spec/models/project_services/redmine_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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 RedmineService, models: true do
diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb
index 7fcfdf0eacd..452f4e2782c 100644
--- a/spec/models/project_services/slack_service/build_message_spec.rb
+++ b/spec/models/project_services/slack_service/build_message_spec.rb
@@ -10,7 +10,7 @@ describe SlackService::BuildMessage do
tag: false,
project_name: 'project_name',
- project_url: 'somewhere.com',
+ project_url: 'example.gitlab.com',
commit: {
status: status,
@@ -20,42 +20,38 @@ describe SlackService::BuildMessage do
}
end
- context 'succeeded' do
+ let(:message) { build_message }
+
+ context 'build succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
let(:duration) { 10 }
-
+ let(:message) { build_message('passed') }
+
it 'returns a message with information about succeeded build' do
- message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 seconds'
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
end
- context 'failed' do
+ context 'build failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
let(:duration) { 10 }
it 'returns a message with information about failed build' do
- message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 seconds'
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
- end
-
- describe '#seconds_name' do
- let(:status) { 'failed' }
- let(:color) { 'danger' }
- let(:duration) { 1 }
+ end
- it 'returns seconds as singular when there is only one' do
- message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 1 second'
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
+ def build_message(status_text = status)
+ "<example.gitlab.com|project_name>:" \
+ " Commit <example.gitlab.com/commit/" \
+ "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
+ " of <example.gitlab.com/commits/develop|develop> branch" \
+ " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end
end
diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb
new file mode 100644
index 00000000000..babb3909f56
--- /dev/null
+++ b/spec/models/project_services/slack_service/pipeline_message_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe SlackService::PipelineMessage do
+ subject { SlackService::PipelineMessage.new(args) }
+
+ let(:args) do
+ {
+ object_attributes: {
+ id: 123,
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ tag: false,
+ ref: 'develop',
+ status: status,
+ duration: duration
+ },
+ project: { path_with_namespace: 'project_name',
+ web_url: 'example.gitlab.com' },
+ commit: { author_name: 'hacker' }
+ }
+ end
+
+ let(:message) { build_message }
+
+ context 'pipeline succeeded' do
+ let(:status) { 'success' }
+ let(:color) { 'good' }
+ let(:duration) { 10 }
+ let(:message) { build_message('passed') }
+
+ it 'returns a message with information about succeeded build' do
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+ end
+
+ context 'pipeline failed' do
+ let(:status) { 'failed' }
+ let(:color) { 'danger' }
+ let(:duration) { 10 }
+
+ it 'returns a message with information about failed build' do
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+ end
+
+ def build_message(status_text = status)
+ "<example.gitlab.com|project_name>:" \
+ " Pipeline <example.gitlab.com/pipelines/123|97de212e>" \
+ " of <example.gitlab.com/commits/develop|develop> branch" \
+ " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+ end
+end
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index 28af68d13b4..c07a70a8069 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -1,26 +1,9 @@
-# == 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 SlackService, models: true do
+ let(:slack) { SlackService.new }
+ let(:webhook_url) { 'https://example.gitlab.com/' }
+
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -42,15 +25,14 @@ describe SlackService, models: true do
end
describe "Execute" do
- let(:slack) { SlackService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
+ let(:username) { 'slack_username' }
+ let(:channel) { 'slack_channel' }
+
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
- let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
- let(:username) { 'slack_username' }
- let(:channel) { 'slack_channel' }
before do
allow(slack).to receive_messages(
@@ -212,10 +194,8 @@ describe SlackService, models: true do
end
describe "Note events" do
- let(:slack) { SlackService.new }
let(:user) { create(:user) }
let(:project) { create(:project, creator_id: user.id) }
- let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
before do
allow(slack).to receive_messages(
@@ -285,4 +265,63 @@ describe SlackService, models: true do
end
end
end
+
+ describe 'Pipeline events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project, status: status,
+ sha: project.commit.sha, ref: project.default_branch)
+ end
+
+ before do
+ allow(slack).to receive_messages(
+ project: project,
+ service_hook: true,
+ webhook: webhook_url
+ )
+ end
+
+ shared_examples 'call Slack API' do
+ before do
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ it 'calls Slack API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ slack.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'with failed pipeline' do
+ let(:status) { 'failed' }
+
+ it_behaves_like 'call Slack API'
+ end
+
+ context 'with succeeded pipeline' do
+ let(:status) { 'success' }
+
+ context 'with default to notify_only_broken_pipelines' do
+ it 'does not call Slack API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ result = slack.execute(data)
+
+ expect(result).to be_falsy
+ end
+ end
+
+ context 'with setting notify_only_broken_pipelines to false' do
+ before do
+ slack.notify_only_broken_pipelines = false
+ end
+
+ it_behaves_like 'call Slack API'
+ end
+ end
+ end
end
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index 474715d24c3..f7e878844dc 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -1,23 +1,3 @@
-# == 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
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 4a41fafb84d..ef854a25321 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -6,6 +6,7 @@ describe Project, models: true do
it { is_expected.to belong_to(:namespace) }
it { is_expected.to belong_to(:creator).class_name('User') }
it { is_expected.to have_many(:users) }
+ it { is_expected.to have_many(:services) }
it { is_expected.to have_many(:events).dependent(:destroy) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
it { is_expected.to have_many(:issues).dependent(:destroy) }
@@ -24,6 +25,30 @@ describe Project, models: true do
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
it { is_expected.to have_one(:board).dependent(:destroy) }
+ it { is_expected.to have_one(:campfire_service).dependent(:destroy) }
+ it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) }
+ it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
+ it { is_expected.to have_one(:builds_email_service).dependent(:destroy) }
+ it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
+ it { is_expected.to have_one(:irker_service).dependent(:destroy) }
+ it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) }
+ it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
+ it { is_expected.to have_one(:flowdock_service).dependent(:destroy) }
+ it { is_expected.to have_one(:assembla_service).dependent(:destroy) }
+ it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) }
+ it { is_expected.to have_one(:buildkite_service).dependent(:destroy) }
+ it { is_expected.to have_one(:bamboo_service).dependent(:destroy) }
+ it { is_expected.to have_one(:teamcity_service).dependent(:destroy) }
+ it { is_expected.to have_one(:jira_service).dependent(:destroy) }
+ it { is_expected.to have_one(:redmine_service).dependent(:destroy) }
+ it { is_expected.to have_one(:custom_issue_tracker_service).dependent(:destroy) }
+ it { is_expected.to have_one(:bugzilla_service).dependent(:destroy) }
+ it { is_expected.to have_one(:gitlab_issue_tracker_service).dependent(:destroy) }
+ it { is_expected.to have_one(:external_wiki_service).dependent(:destroy) }
+ it { is_expected.to have_one(:project_feature).dependent(:destroy) }
+ it { is_expected.to have_one(:import_data).class_name('ProjectImportData').dependent(:destroy) }
+ it { is_expected.to have_one(:last_event).class_name('Event') }
+ it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) }
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:builds) }
@@ -31,12 +56,19 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
+ it { is_expected.to have_many(:labels).dependent(:destroy) }
+ it { is_expected.to have_many(:users_star_projects).dependent(:destroy) }
it { is_expected.to have_many(:environments).dependent(:destroy) }
it { is_expected.to have_many(:deployments).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
+ it { is_expected.to have_many(:releases).dependent(:destroy) }
+ it { is_expected.to have_many(:lfs_objects_projects).dependent(:destroy) }
+ it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
+ it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
+ it { is_expected.to have_many(:forks).through(:forked_project_links) }
describe '#members & #requesters' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
let(:requester) { create(:user) }
let(:developer) { create(:user) }
before do
@@ -178,7 +210,7 @@ describe Project, models: true do
expect(project.runners_token).not_to eq('')
end
- it 'does not set an random toke if one provided' do
+ it 'does not set an random token if one provided' do
project = FactoryGirl.create :empty_project, runners_token: 'my-token'
expect(project.runners_token).to eq('my-token')
end
@@ -276,20 +308,23 @@ describe Project, models: true do
end
describe 'last_activity methods' do
- let(:project) { create(:project) }
- let(:last_event) { double(created_at: Time.now) }
+ let(:timestamp) { Time.now - 2.hours }
+ let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
describe 'last_activity' do
it 'alias last_activity to last_event' do
- allow(project).to receive(:last_event).and_return(last_event)
+ last_event = create(:event, project: project)
+
expect(project.last_activity).to eq(last_event)
end
end
describe 'last_activity_date' do
it 'returns the creation date of the project\'s last event if present' do
- create(:event, project: project)
- expect(project.last_activity_at.to_i).to eq(last_event.created_at.to_i)
+ expect_any_instance_of(Event).to receive(:try_obtain_lease).and_return(true)
+ new_event = create(:event, project: project, created_at: Time.now)
+
+ expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
end
it 'returns the project\'s last update date if it has no events' do
@@ -801,7 +836,7 @@ describe Project, models: true do
describe 'when a user has access to a project' do
before do
- project.team.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MASTER)
end
it { is_expected.to eq([project]) }
@@ -1385,6 +1420,68 @@ describe Project, models: true do
end
end
+ describe '#lfs_enabled?' do
+ let(:project) { create(:project) }
+
+ shared_examples 'project overrides group' do
+ it 'returns true when enabled in project' do
+ project.update_attribute(:lfs_enabled, true)
+
+ expect(project.lfs_enabled?).to be_truthy
+ end
+
+ it 'returns false when disabled in project' do
+ project.update_attribute(:lfs_enabled, false)
+
+ expect(project.lfs_enabled?).to be_falsey
+ end
+
+ it 'returns the value from the namespace, when no value is set in project' do
+ expect(project.lfs_enabled?).to eq(project.namespace.lfs_enabled?)
+ end
+ end
+
+ context 'LFS disabled in group' do
+ before do
+ project.namespace.update_attribute(:lfs_enabled, false)
+ enable_lfs
+ end
+
+ it_behaves_like 'project overrides group'
+ end
+
+ context 'LFS enabled in group' do
+ before do
+ project.namespace.update_attribute(:lfs_enabled, true)
+ enable_lfs
+ end
+
+ it_behaves_like 'project overrides group'
+ end
+
+ describe 'LFS disabled globally' do
+ shared_examples 'it always returns false' do
+ it do
+ expect(project.lfs_enabled?).to be_falsey
+ expect(project.namespace.lfs_enabled?).to be_falsey
+ end
+ end
+
+ context 'when no values are set' do
+ it_behaves_like 'it always returns false'
+ end
+
+ context 'when all values are set to true' do
+ before do
+ project.namespace.update_attribute(:lfs_enabled, true)
+ project.update_attribute(:lfs_enabled, true)
+ end
+
+ it_behaves_like 'it always returns false'
+ end
+ end
+ end
+
describe '.where_paths_in' do
context 'without any paths' do
it 'returns an empty relation' do
@@ -1497,4 +1594,101 @@ describe Project, models: true do
project.change_head(project.default_branch)
end
end
+
+ describe '#pushes_since_gc' do
+ let(:project) { create(:project) }
+
+ after do
+ project.reset_pushes_since_gc
+ end
+
+ context 'without any pushes' do
+ it 'returns 0' do
+ expect(project.pushes_since_gc).to eq(0)
+ end
+ end
+
+ context 'with a number of pushes' do
+ it 'returns the number of pushes' do
+ 3.times { project.increment_pushes_since_gc }
+
+ expect(project.pushes_since_gc).to eq(3)
+ end
+ end
+ end
+
+ describe '#increment_pushes_since_gc' do
+ let(:project) { create(:project) }
+
+ after do
+ project.reset_pushes_since_gc
+ end
+
+ it 'increments the number of pushes since the last GC' do
+ 3.times { project.increment_pushes_since_gc }
+
+ expect(project.pushes_since_gc).to eq(3)
+ end
+ end
+
+ describe '#reset_pushes_since_gc' do
+ let(:project) { create(:project) }
+
+ after do
+ project.reset_pushes_since_gc
+ end
+
+ it 'resets the number of pushes since the last GC' do
+ 3.times { project.increment_pushes_since_gc }
+
+ project.reset_pushes_since_gc
+
+ expect(project.pushes_since_gc).to eq(0)
+ end
+ end
+
+ describe '#environments_for' do
+ let(:project) { create(:project) }
+ let(:environment) { create(:environment, project: project) }
+
+ context 'tagged deployment' do
+ before do
+ create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
+ end
+
+ it 'returns environment when with_tags is set' do
+ expect(project.environments_for('master', project.commit, with_tags: true)).to contain_exactly(environment)
+ end
+
+ it 'does not return environment when no with_tags is set' do
+ expect(project.environments_for('master', project.commit)).to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(project.environments_for('master', project.commit('feature'))).to be_empty
+ end
+ end
+
+ context 'branch deployment' do
+ before do
+ create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ end
+
+ it 'returns environment when ref is set' do
+ expect(project.environments_for('master', project.commit)).to contain_exactly(environment)
+ end
+
+ it 'does not environment when ref is different' do
+ expect(project.environments_for('feature', project.commit)).to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(project.environments_for('master', project.commit('feature'))).to be_empty
+ end
+ end
+ end
+
+ def enable_lfs
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 5eaf0d3b7a6..e0f2dadf189 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -73,9 +73,71 @@ describe ProjectTeam, models: true do
end
end
- describe '#find_member' do
+ describe '#fetch_members' do
context 'personal project' do
let(:project) { create(:empty_project) }
+
+ it 'returns project members' do
+ user = create(:user)
+ project.team << [user, :guest]
+
+ expect(project.team.members).to contain_exactly(user)
+ end
+
+ it 'returns project members of a specified level' do
+ user = create(:user)
+ project.team << [user, :reporter]
+
+ expect(project.team.guests).to be_empty
+ expect(project.team.reporters).to contain_exactly(user)
+ end
+
+ it 'returns invited members of a group' do
+ group_member = create(:group_member)
+
+ project.project_group_links.create!(
+ group: group_member.group,
+ group_access: Gitlab::Access::GUEST
+ )
+
+ expect(project.team.members).to contain_exactly(group_member.user)
+ end
+
+ it 'returns invited members of a group of a specified level' do
+ group_member = create(:group_member)
+
+ project.project_group_links.create!(
+ group: group_member.group,
+ group_access: Gitlab::Access::REPORTER
+ )
+
+ expect(project.team.guests).to be_empty
+ expect(project.team.reporters).to contain_exactly(group_member.user)
+ end
+ end
+
+ context 'group project' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, group: group) }
+
+ it 'returns project members' do
+ group_member = create(:group_member, group: group)
+
+ expect(project.team.members).to contain_exactly(group_member.user)
+ end
+
+ it 'returns project members of a specified level' do
+ group_member = create(:group_member, :reporter, group: group)
+
+ expect(project.team.guests).to be_empty
+ expect(project.team.reporters).to contain_exactly(group_member.user)
+ end
+ end
+ end
+
+ describe '#find_member' do
+ context 'personal project' do
+ let(:project) { create(:empty_project, :public) }
let(:requester) { create(:user) }
before do
@@ -138,7 +200,7 @@ describe ProjectTeam, models: true do
let(:requester) { create(:user) }
context 'personal project' do
- let(:project) { create(:empty_project) }
+ let(:project) { create(:empty_project, :public) }
context 'when project is not shared with group' do
before do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 812c72c48cb..db29f4d353b 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -16,6 +16,21 @@ describe Repository, models: true do
merge_commit_id = repository.merge(user, merge_request, commit_options)
repository.commit(merge_commit_id)
end
+ let(:author_email) { FFaker::Internet.email }
+
+ # I have to remove periods from the end of the name
+ # This happened when the user's name had a suffix (i.e. "Sr.")
+ # This seems to be what git does under the hood. For example, this commit:
+ #
+ # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
+ #
+ # results in this:
+ #
+ # $ git show --pretty
+ # ...
+ # Author: Foo Sr <foo@example.com>
+ # ...
+ let(:author_name) { FFaker::Name.name.chomp("\.") }
describe '#branch_names_contains' do
subject { repository.branch_names_contains(sample_commit.id) }
@@ -132,7 +147,31 @@ describe Repository, models: true do
end
end
- describe :commit_file do
+ describe "#commit_dir" do
+ it "commits a change that creates a new directory" do
+ expect do
+ repository.commit_dir(user, 'newdir', 'Create newdir', 'master')
+ end.to change { repository.commits('master').count }.by(1)
+
+ newdir = repository.tree('master', 'newdir')
+ expect(newdir.path).to eq('newdir')
+ end
+
+ context "when an author is specified" do
+ it "uses the given email/name to set the commit's author" do
+ expect do
+ repository.commit_dir(user, "newdir", "Add newdir", 'master', author_email: author_email, author_name: author_name)
+ end.to change { repository.commits('master').count }.by(1)
+
+ last_commit = repository.commit
+
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
+ end
+
+ describe "#commit_file" do
it 'commits change to a file successfully' do
expect do
repository.commit_file(user, 'CHANGELOG', 'Changelog!',
@@ -144,9 +183,23 @@ describe Repository, models: true do
expect(blob.data).to eq('Changelog!')
end
+
+ context "when an author is specified" do
+ it "uses the given email/name to set the commit's author" do
+ expect do
+ repository.commit_file(user, "README", 'README!', 'Add README',
+ 'master', true, author_email: author_email, author_name: author_name)
+ end.to change { repository.commits('master').count }.by(1)
+
+ last_commit = repository.commit
+
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
end
- describe :update_file do
+ describe "#update_file" do
it 'updates filename successfully' do
expect do
repository.update_file(user, 'NEWLICENSE', 'Copyright!',
@@ -160,6 +213,85 @@ describe Repository, models: true do
expect(files).not_to include('LICENSE')
expect(files).to include('NEWLICENSE')
end
+
+ context "when an author is specified" do
+ it "uses the given email/name to set the commit's author" do
+ repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+
+ expect do
+ repository.update_file(user, 'README', "Updated README!",
+ branch: 'master',
+ previous_path: 'README',
+ message: 'Update README',
+ author_email: author_email,
+ author_name: author_name)
+ end.to change { repository.commits('master').count }.by(1)
+
+ last_commit = repository.commit
+
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
+ end
+
+ describe "#remove_file" do
+ it 'removes file successfully' do
+ repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+
+ expect do
+ repository.remove_file(user, "README", "Remove README", 'master')
+ end.to change { repository.commits('master').count }.by(1)
+
+ expect(repository.blob_at('master', 'README')).to be_nil
+ end
+
+ context "when an author is specified" do
+ it "uses the given email/name to set the commit's author" do
+ repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+
+ expect do
+ repository.remove_file(user, "README", "Remove README", 'master', author_email: author_email, author_name: author_name)
+ end.to change { repository.commits('master').count }.by(1)
+
+ last_commit = repository.commit
+
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
+ end
+
+ describe '#get_committer_and_author' do
+ it 'returns the committer and author data' do
+ options = repository.get_committer_and_author(user)
+ expect(options[:committer][:email]).to eq(user.email)
+ expect(options[:author][:email]).to eq(user.email)
+ end
+
+ context 'when the email/name are given' do
+ it 'returns an object containing the email/name' do
+ options = repository.get_committer_and_author(user, email: author_email, name: author_name)
+ expect(options[:author][:email]).to eq(author_email)
+ expect(options[:author][:name]).to eq(author_name)
+ end
+ end
+
+ context 'when the email is given but the name is not' do
+ it 'returns the committer as the author' do
+ options = repository.get_committer_and_author(user, email: author_email)
+ expect(options[:author][:email]).to eq(user.email)
+ expect(options[:author][:name]).to eq(user.name)
+ end
+ end
+
+ context 'when the name is given but the email is not' do
+ it 'returns nil' do
+ options = repository.get_committer_and_author(user, name: author_name)
+ expect(options[:author][:email]).to eq(user.email)
+ expect(options[:author][:name]).to eq(user.name)
+ end
+ end
end
describe "search_files" do
@@ -186,32 +318,6 @@ describe Repository, models: true do
it { is_expected.to be_an String }
it { expect(subject.lines[2]).to eq("master:CHANGELOG:188: - Feature: Replace teams with group membership\n") }
end
-
- describe 'parsing result' do
- subject { repository.parse_search_result(search_result) }
- let(:search_result) { results.first }
-
- it { is_expected.to be_an OpenStruct }
- it { expect(subject.filename).to eq('CHANGELOG') }
- it { expect(subject.basename).to eq('CHANGELOG') }
- it { expect(subject.ref).to eq('master') }
- it { expect(subject.startline).to eq(186) }
- it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") }
-
- context "when filename has extension" do
- let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
-
- it { expect(subject.filename).to eq('CONTRIBUTE.md') }
- it { expect(subject.basename).to eq('CONTRIBUTE') }
- end
-
- context "when file under directory" do
- let(:search_result) { "master:a/b/c.md:5:a b c\n" }
-
- it { expect(subject.filename).to eq('a/b/c.md') }
- it { expect(subject.basename).to eq('a/b/c') }
- end
- end
end
describe "#changelog" do
@@ -441,43 +547,77 @@ describe Repository, models: true do
end
end
- describe '#commit_with_hooks' do
+ describe '#update_branch_with_hooks' do
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
+ let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev
context 'when pre hooks were successful' do
before do
expect_any_instance_of(GitHooksService).to receive(:execute).
- with(user, repository.path_to_repo, old_rev, sample_commit.id, 'refs/heads/feature').
+ with(user, repository.path_to_repo, old_rev, new_rev, 'refs/heads/feature').
and_yield.and_return(true)
end
it 'runs without errors' do
expect do
- repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+ repository.update_branch_with_hooks(user, 'feature') { new_rev }
end.not_to raise_error
end
it 'ensures the autocrlf Git option is set to :input' do
expect(repository).to receive(:update_autocrlf_option)
- repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+ repository.update_branch_with_hooks(user, 'feature') { new_rev }
end
context "when the branch wasn't empty" do
it 'updates the head' do
expect(repository.find_branch('feature').target.id).to eq(old_rev)
- repository.commit_with_hooks(user, 'feature') { sample_commit.id }
- expect(repository.find_branch('feature').target.id).to eq(sample_commit.id)
+ repository.update_branch_with_hooks(user, 'feature') { new_rev }
+ expect(repository.find_branch('feature').target.id).to eq(new_rev)
end
end
end
+ context 'when the update adds more than one commit' do
+ it 'runs without errors' do
+ old_rev = '33f3729a45c02fc67d00adb1b8bca394b0e761d9'
+
+ # old_rev is an ancestor of new_rev
+ expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev)
+
+ # old_rev is not a direct ancestor (parent) of new_rev
+ expect(repository.rugged.lookup(new_rev).parent_ids).not_to include(old_rev)
+
+ branch = 'feature-ff-target'
+ repository.add_branch(user, branch, old_rev)
+
+ expect { repository.update_branch_with_hooks(user, branch) { new_rev } }.not_to raise_error
+ end
+ end
+
+ context 'when the update would remove commits from the target branch' do
+ it 'raises an exception' do
+ branch = 'master'
+ old_rev = repository.find_branch(branch).target.sha
+
+ # The 'master' branch is NOT an ancestor of new_rev.
+ expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev)
+
+ # Updating 'master' to new_rev would lose the commits on 'master' that
+ # are not contained in new_rev. This should not be allowed.
+ expect do
+ repository.update_branch_with_hooks(user, branch) { new_rev }
+ end.to raise_error(Repository::CommitError)
+ end
+ end
+
context 'when pre hooks failed' do
it 'gets 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 }
+ repository.update_branch_with_hooks(user, 'feature') { new_rev }
end.to raise_error(GitHooksService::PreReceiveError)
end
end
@@ -485,6 +625,7 @@ describe Repository, models: true do
context 'when target branch is different from source branch' do
before do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
+ allow(repository).to receive(:update_ref!)
end
it 'expires branch cache' do
@@ -495,7 +636,7 @@ describe Repository, models: true do
expect(repository).to receive(:expire_has_visible_content_cache)
expect(repository).to receive(:expire_branch_count_cache)
- repository.commit_with_hooks(user, 'new-feature') { sample_commit.id }
+ repository.update_branch_with_hooks(user, 'new-feature') { new_rev }
end
end
@@ -1268,4 +1409,18 @@ describe Repository, models: true do
File.delete(path)
end
end
+
+ describe '#update_ref!' do
+ it 'can create a ref' do
+ repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+
+ expect(repository.find_branch('foobar')).not_to be_nil
+ end
+
+ it 'raises CommitError when the ref update fails' do
+ expect do
+ repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+ end.to raise_error(Repository::CommitError)
+ end
+ end
end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 0621c6a06ce..e6bc5296398 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -9,12 +9,14 @@ describe Snippet, models: true do
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
+ it { is_expected.to include_module(Awardable) }
end
describe 'associations' do
it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
+ it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
end
describe 'validation' do
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index eda1cafd65e..a7a06744428 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -33,4 +33,17 @@ describe ProjectPolicy, models: true do
it 'returns increasing permissions for each level' do
expect(users_permissions).to eq(users_permissions.sort.uniq)
end
+
+ it 'does not include the read_issue permission when the issue author is not a member of the private project' do
+ project = create(:project, :private)
+ issue = create(:issue, project: project)
+ user = issue.author
+
+ expect(project.team.member?(issue.author)).to eq(false)
+
+ expect(BasePolicy.class_for(project).abilities(user, project).can_set).
+ not_to include(:read_issue)
+
+ expect(Ability.allowed?(user, :read_issue, project)).to be_falsy
+ end
end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index d78494b76fa..905a7311372 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -64,12 +64,12 @@ describe API::AccessRequests, api: true do
context 'when authenticated as a member' do
%i[developer master].each do |type|
context "as a #{type}" do
- it 'returns 400' do
+ it 'returns 403' do
expect do
user = public_send(type)
post api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_http_status(403)
end.not_to change { source.requesters.count }
end
end
@@ -87,6 +87,20 @@ describe API::AccessRequests, api: true do
end
context 'when authenticated as a stranger' do
+ context "when access request is disabled for the #{source_type}" do
+ before do
+ source.update(request_access_enabled: false)
+ end
+
+ it 'returns 403' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
+
+ expect(response).to have_http_status(403)
+ end.not_to change { source.requesters.count }
+ end
+ end
+
it 'returns 201' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index bbdf8f03c2b..e66faeed705 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -36,11 +36,36 @@ describe API::Helpers, api: true do
params.delete(API::Helpers::SUDO_PARAM)
end
+ def warden_authenticate_returns(value)
+ warden = double("warden", authenticate: value)
+ env['warden'] = warden
+ end
+
+ def doorkeeper_guard_returns(value)
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value }
+ end
+
def error!(message, status)
raise Exception
end
describe ".current_user" do
+ subject { current_user }
+
+ describe "when authenticating via Warden" do
+ before { doorkeeper_guard_returns false }
+
+ context "fails" do
+ it { is_expected.to be_nil }
+ end
+
+ context "succeeds" do
+ before { warden_authenticate_returns user }
+
+ it { is_expected.to eq(user) }
+ end
+ end
+
describe "when authenticating using a user's private token" do
it "returns nil for an invalid token" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 981a6791881..5ad4fc4865a 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project) }
+ let!(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
@@ -39,6 +39,19 @@ describe API::API, api: true do
end
end
+ context 'on a snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet) }
+
+ it 'returns the awarded emoji' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(award.name)
+ end
+ end
+
context 'when the user has no access' do
it 'returns a status code 404' do
user1 = create(:user)
@@ -91,6 +104,20 @@ describe API::API, api: true do
end
end
+ context 'on a snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet) }
+
+ it 'returns the awarded emoji' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(award.name)
+ expect(json_response['awardable_id']).to eq(snippet.id)
+ expect(json_response['awardable_type']).to eq("Snippet")
+ end
+ end
+
context 'when the user has no access' do
it 'returns a status code 404' do
user1 = create(:user)
@@ -160,6 +187,18 @@ describe API::API, api: true do
end
end
end
+
+ context 'on a snippet' do
+ it 'creates a new award emoji' do
+ snippet = create(:project_snippet, :public, project: project)
+
+ post api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['name']).to eq('blowfish')
+ expect(json_response['user']['username']).to eq(user.username)
+ end
+ end
end
describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
@@ -229,6 +268,19 @@ describe API::API, api: true do
expect(response).to have_http_status(404)
end
end
+
+ context 'when the awardable is a Snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet, user: user) }
+
+ it 'deletes the award' do
+ expect do
+ delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
+ end.to change { snippet.award_emoji.count }.from(1).to(0)
+
+ expect(response).to have_http_status(200)
+ end
+ end
end
describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index ee0b61e2ca4..95c7bbf99c9 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -30,6 +30,15 @@ describe API::API, api: true do
expect(json_response.first['commit']['id']).to eq project.commit.id
end
+ it 'returns pipeline data' do
+ json_build = json_response.first
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
+
context 'filter project with one scope element' do
let(:query) { 'scope=pending' }
@@ -91,6 +100,15 @@ describe API::API, api: true do
expect(json_response).to be_an Array
expect(json_response.size).to eq 2
end
+
+ it 'returns pipeline data' do
+ json_build = json_response.first
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
end
context 'when pipeline has no builds' do
@@ -133,6 +151,15 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response['name']).to eq('test')
end
+
+ it 'returns pipeline data' do
+ json_build = json_response
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
end
context 'unauthorized user' do
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 2d6093fec7a..7aa7e85a9e2 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -117,17 +117,36 @@ describe API::CommitStatuses, api: true do
let(:post_url) { "/projects/#{project.id}/statuses/#{sha}" }
context 'developer user' do
- context 'only required parameters' do
- before { post api(post_url, developer), state: 'success' }
+ %w[pending running success failed canceled].each do |status|
+ context "for #{status}" do
+ context 'uses only required parameters' do
+ it 'creates commit status' do
+ post api(post_url, developer), state: status
+
+ expect(response).to have_http_status(201)
+ expect(json_response['sha']).to eq(commit.id)
+ expect(json_response['status']).to eq(status)
+ expect(json_response['name']).to eq('default')
+ expect(json_response['ref']).not_to be_empty
+ expect(json_response['target_url']).to be_nil
+ expect(json_response['description']).to be_nil
+ end
+ end
+ end
+ end
- it 'creates commit status' do
- expect(response).to have_http_status(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
+ context 'transitions status from pending' do
+ before do
+ post api(post_url, developer), state: 'pending'
+ end
+
+ %w[running success failed canceled].each do |status|
+ it "to #{status}" do
+ expect { post api(post_url, developer), state: status }.not_to change { CommitStatus.count }
+
+ expect(response).to have_http_status(201)
+ expect(json_response['status']).to eq(status)
+ end
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 5b3dc60aba2..10f772c5b1a 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -110,7 +110,7 @@ describe API::API, api: true do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
expect(response).to have_http_status(200)
- expect(json_response['status']).to be_nil
+ expect(json_response['status']).to eq("created")
end
end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 2d1213df8a7..050d0dd082d 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -5,6 +5,21 @@ describe API::API, api: true do
let(:user) { create(:user) }
let!(:project) { create(:project, namespace: user.namespace ) }
let(:file_path) { 'files/ruby/popen.rb' }
+ let(:author_email) { FFaker::Internet.email }
+
+ # I have to remove periods from the end of the name
+ # This happened when the user's name had a suffix (i.e. "Sr.")
+ # This seems to be what git does under the hood. For example, this commit:
+ #
+ # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
+ #
+ # results in this:
+ #
+ # $ git show --pretty
+ # ...
+ # Author: Foo Sr <foo@example.com>
+ # ...
+ let(:author_name) { FFaker::Name.name.chomp("\.") }
before { project.team << [user, :developer] }
@@ -16,6 +31,7 @@ describe API::API, api: true do
}
get api("/projects/#{project.id}/repository/files", user), params
+
expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq('popen.rb')
@@ -25,6 +41,7 @@ describe API::API, api: true do
it "returns a 400 bad request if no params given" do
get api("/projects/#{project.id}/repository/files", user)
+
expect(response).to have_http_status(400)
end
@@ -35,6 +52,7 @@ describe API::API, api: true do
}
get api("/projects/#{project.id}/repository/files", user), params
+
expect(response).to have_http_status(404)
end
end
@@ -51,12 +69,17 @@ describe API::API, api: true do
it "creates a new file in project repo" do
post api("/projects/#{project.id}/repository/files", user), valid_params
+
expect(response).to have_http_status(201)
expect(json_response['file_path']).to eq('newfile.rb')
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
end
it "returns a 400 bad request if no params given" do
post api("/projects/#{project.id}/repository/files", user)
+
expect(response).to have_http_status(400)
end
@@ -65,8 +88,22 @@ describe API::API, api: true do
and_return(false)
post api("/projects/#{project.id}/repository/files", user), valid_params
+
expect(response).to have_http_status(400)
end
+
+ context "when specifying an author" do
+ it "creates a new file with the specified author" do
+ valid_params.merge!(author_email: author_email, author_name: author_name)
+
+ post api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(201)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
end
describe "PUT /projects/:id/repository/files" do
@@ -81,14 +118,32 @@ describe API::API, api: true do
it "updates existing file in project repo" do
put api("/projects/#{project.id}/repository/files", user), valid_params
+
expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
end
it "returns a 400 bad request if no params given" do
put api("/projects/#{project.id}/repository/files", user)
+
expect(response).to have_http_status(400)
end
+
+ context "when specifying an author" do
+ it "updates a file with the specified author" do
+ valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content")
+
+ put api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(200)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
end
describe "DELETE /projects/:id/repository/files" do
@@ -102,12 +157,17 @@ describe API::API, api: true do
it "deletes existing file in project repo" do
delete api("/projects/#{project.id}/repository/files", user), valid_params
+
expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
end
it "returns a 400 bad request if no params given" do
delete api("/projects/#{project.id}/repository/files", user)
+
expect(response).to have_http_status(400)
end
@@ -115,8 +175,22 @@ describe API::API, api: true do
allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
delete api("/projects/#{project.id}/repository/files", user), valid_params
+
expect(response).to have_http_status(400)
end
+
+ context "when specifying an author" do
+ it "removes a file with the specified author" do
+ valid_params.merge!(author_email: author_email, author_name: author_name)
+
+ delete api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(200)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
end
describe "POST /projects/:id/repository/files with binary file" do
@@ -143,6 +217,7 @@ describe API::API, api: true do
it "remains unchanged" do
get api("/projects/#{project.id}/repository/files", user), get_params
+
expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq(file_path)
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index f802fcd2d2e..34f84f78952 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -6,6 +6,12 @@ describe API::API, api: true do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
+ let(:group) { create(:group) }
+ let(:group2) do
+ group = create(:group, name: 'group2_name')
+ group.add_owner(user2)
+ group
+ end
let(:project) do
create(:project, creator_id: user.id, namespace: user.namespace)
@@ -22,6 +28,7 @@ describe API::API, api: true do
context 'when authenticated' do
it 'forks if user has sufficient access to project' do
post api("/projects/fork/#{project.id}", user2)
+
expect(response).to have_http_status(201)
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq(project.path)
@@ -32,6 +39,7 @@ describe API::API, api: true do
it 'forks if user is admin' do
post api("/projects/fork/#{project.id}", admin)
+
expect(response).to have_http_status(201)
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq(project.path)
@@ -42,12 +50,14 @@ describe API::API, api: true do
it 'fails on missing project access for the project to fork' do
post api("/projects/fork/#{project.id}", user3)
+
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'fails if forked project exists in the user namespace' do
post api("/projects/fork/#{project.id}", user)
+
expect(response).to have_http_status(409)
expect(json_response['message']['name']).to eq(['has already been taken'])
expect(json_response['message']['path']).to eq(['has already been taken'])
@@ -55,14 +65,70 @@ describe API::API, api: true do
it 'fails if project to fork from does not exist' do
post api('/projects/fork/424242', user)
+
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
+
+ it 'forks with explicit own user namespace id' do
+ post api("/projects/fork/#{project.id}", user2), namespace: user2.namespace.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['owner']['id']).to eq(user2.id)
+ end
+
+ it 'forks with explicit own user name as namespace' do
+ post api("/projects/fork/#{project.id}", user2), namespace: user2.username
+
+ expect(response).to have_http_status(201)
+ expect(json_response['owner']['id']).to eq(user2.id)
+ end
+
+ it 'forks to another user when admin' do
+ post api("/projects/fork/#{project.id}", admin), namespace: user2.username
+
+ expect(response).to have_http_status(201)
+ expect(json_response['owner']['id']).to eq(user2.id)
+ end
+
+ it 'fails if trying to fork to another user when not admin' do
+ post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'fails if trying to fork to non-existent namespace' do
+ post api("/projects/fork/#{project.id}", user2), namespace: 42424242
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Target Namespace Not Found')
+ end
+
+ it 'forks to owned group' do
+ post api("/projects/fork/#{project.id}", user2), namespace: group2.name
+
+ expect(response).to have_http_status(201)
+ expect(json_response['namespace']['name']).to eq(group2.name)
+ end
+
+ it 'fails to fork to not owned group' do
+ post api("/projects/fork/#{project.id}", user2), namespace: group.name
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'forks to not owned group when admin' do
+ post api("/projects/fork/#{project.id}", admin), namespace: group.name
+
+ expect(response).to have_http_status(201)
+ expect(json_response['namespace']['name']).to eq(group.name)
+ end
end
context 'when unauthenticated' do
it 'returns authentication error' do
post api("/projects/fork/#{project.id}")
+
expect(response).to have_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 4860b23c2ed..1f68ef1af8f 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -120,10 +120,11 @@ describe API::API, api: true do
context 'when authenticated as the group owner' do
it 'updates the group' do
- put api("/groups/#{group1.id}", user1), name: new_group_name
+ put api("/groups/#{group1.id}", user1), name: new_group_name, request_access_enabled: true
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(new_group_name)
+ expect(json_response['request_access_enabled']).to eq(true)
end
it 'returns 404 for a non existing group' do
@@ -238,8 +239,14 @@ describe API::API, api: true do
context "when authenticated as user with group permissions" do
it "creates group" do
- post api("/groups", user3), attributes_for(:group)
+ group = attributes_for(:group, { request_access_enabled: false })
+
+ post api("/groups", user3), group
expect(response).to have_http_status(201)
+
+ expect(json_response["name"]).to eq(group[:name])
+ expect(json_response["path"]).to eq(group[:path])
+ expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled])
end
it "does not create group, duplicate" do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 5d06abcfeb3..f0f590b0331 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -44,8 +44,8 @@ describe API::API, api: true do
secret_token: secret_token,
key_id: 12345
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Could not find the given key')
end
it 'returns an error message when the key is a deploy key' do
@@ -100,6 +100,43 @@ describe API::API, api: true do
end
end
+ describe "POST /internal/lfs_authenticate" do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'user key' do
+ it 'returns the correct information about the key' do
+ lfs_auth(key.id, project)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['username']).to eq(user.username)
+ expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token)
+
+ expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
+ end
+
+ it 'returns a 404 when the wrong key is provided' do
+ lfs_auth(nil, project)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'deploy key' do
+ let(:key) { create(:deploy_key) }
+
+ it 'returns the correct information about the key' do
+ lfs_auth(key.id, project)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}")
+ expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token)
+ expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
+ end
+ end
+ end
+
describe "GET /internal/discover" do
it do
get(api("/internal/discover"), key_id: key.id, secret_token: secret_token)
@@ -389,4 +426,13 @@ describe API::API, api: true do
protocol: 'ssh'
)
end
+
+ def lfs_auth(key_id, project)
+ post(
+ api("/internal/lfs_authenticate"),
+ key_id: key_id,
+ secret_token: secret_token,
+ project: project.path_with_namespace
+ )
+ end
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 47344a13b5e..f840778ae9b 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -17,21 +17,27 @@ describe API::API, api: true do
assignee: user,
project: project,
state: :closed,
- milestone: milestone
+ milestone: milestone,
+ created_at: generate(:issue_created_at),
+ updated_at: 3.hours.ago
end
let!(:confidential_issue) do
create :issue,
:confidential,
project: project,
author: author,
- assignee: assignee
+ assignee: assignee,
+ created_at: generate(:issue_created_at),
+ updated_at: 2.hours.ago
end
let!(:issue) do
create :issue,
author: user,
assignee: user,
project: project,
- milestone: milestone
+ milestone: milestone,
+ created_at: generate(:issue_created_at),
+ updated_at: 1.hour.ago
end
let!(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
@@ -135,6 +141,42 @@ describe API::API, api: true do
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
+
+ it 'sorts by created_at descending by default' do
+ get api('/issues', user)
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get api('/issues?sort=asc', user)
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get api('/issues?order_by=updated_at', user)
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get api('/issues?order_by=updated_at&sort=asc', user)
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
end
end
@@ -147,21 +189,24 @@ describe API::API, api: true do
assignee: user,
project: group_project,
state: :closed,
- milestone: group_milestone
+ milestone: group_milestone,
+ updated_at: 3.hours.ago
end
let!(:group_confidential_issue) do
create :issue,
:confidential,
project: group_project,
author: author,
- assignee: assignee
+ assignee: assignee,
+ updated_at: 2.hours.ago
end
let!(:group_issue) do
create :issue,
author: user,
assignee: user,
project: group_project,
- milestone: group_milestone
+ milestone: group_milestone,
+ updated_at: 1.hour.ago
end
let!(:group_label) do
create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
@@ -278,6 +323,42 @@ describe API::API, api: true do
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(group_closed_issue.id)
end
+
+ it 'sorts by created_at descending by default' do
+ get api(base_url, user)
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get api("#{base_url}?sort=asc", user)
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get api("#{base_url}?order_by=updated_at", user)
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get api("#{base_url}?order_by=updated_at&sort=asc", user)
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
end
describe "GET /projects/:id/issues" do
@@ -386,6 +467,42 @@ describe API::API, api: true do
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_issue.id)
end
+
+ it 'sorts by created_at descending by default' do
+ get api("#{base_url}/issues", user)
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get api("#{base_url}/issues?sort=asc", user)
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get api("#{base_url}/issues?order_by=updated_at", user)
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get api("#{base_url}/issues?order_by=updated_at&sort=asc", user)
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
end
describe "GET /projects/:id/issues/:issue_id" do
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
new file mode 100644
index 00000000000..391fc13a380
--- /dev/null
+++ b/spec/requests/api/lint_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe API::Lint, api: true do
+ include ApiHelpers
+
+ describe 'POST /ci/lint' do
+ context 'with valid .gitlab-ci.yaml content' do
+ let(:yaml_content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ end
+
+ it 'passes validation' do
+ post api('/ci/lint'), { content: yaml_content }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Hash
+ expect(json_response['status']).to eq('valid')
+ expect(json_response['errors']).to eq([])
+ end
+ end
+
+ context 'with an invalid .gitlab_ci.yml' do
+ it 'responds with errors about invalid syntax' do
+ post api('/ci/lint'), { content: 'invalid content' }
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq('invalid')
+ expect(json_response['errors']).to eq(['Invalid configuration format'])
+ end
+
+ it "responds with errors about invalid configuration" do
+ post api('/ci/lint'), { content: '{ image: "ruby:2.1", services: ["postgres"] }' }
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq('invalid')
+ expect(json_response['errors']).to eq(['jobs config should contain at least one visible job'])
+ end
+ end
+
+ context 'without the content parameter' do
+ it 'responds with validation error about missing content' do
+ post api('/ci/lint')
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('content is missing')
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 1e365bf353a..92032f09b17 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -30,20 +30,29 @@ describe API::Members, api: true do
let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
end
- context 'when authenticated as a non-member' do
- %i[access_requester stranger].each do |type|
- context "as a #{type}" do
- it 'returns 200' do
- user = public_send(type)
- get api("/#{source_type.pluralize}/#{source.id}/members", user)
+ %i[master developer access_requester stranger].each do |type|
+ context "when authenticated as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+ get api("/#{source_type.pluralize}/#{source.id}/members", user)
- expect(response).to have_http_status(200)
- expect(json_response.size).to eq(2)
- end
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
end
end
end
+ it 'does not return invitees' do
+ create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil)
+
+ get api("/#{source_type.pluralize}/#{source.id}/members", developer)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
+ end
+
it 'finds members with query string' do
get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index a7930c59df9..b813ee967f8 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -15,7 +15,7 @@ describe API::API, api: true do
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
before do
- project.team << [user, :reporters]
+ project.team << [user, :reporter]
end
describe "GET /projects/:id/merge_requests" do
@@ -299,7 +299,7 @@ describe API::API, api: true do
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
before :each do |each|
- fork_project.team << [user2, :reporters]
+ fork_project.team << [user2, :reporter]
end
it "returns merge_request" do
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index d6a0c656e74..dd192bea432 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project, namespace: user.namespace ) }
+ let!(:project) { create(:empty_project, namespace: user.namespace ) }
let!(:closed_milestone) { create(:closed_milestone, project: project) }
let!(:milestone) { create(:milestone, project: project) }
@@ -12,6 +12,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/milestones' do
it 'returns project milestones' do
get api("/projects/#{project.id}/milestones", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(milestone.title)
@@ -19,6 +20,7 @@ describe API::API, api: true do
it 'returns a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones")
+
expect(response).to have_http_status(401)
end
@@ -44,6 +46,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/milestones/:milestone_id' do
it 'returns a project milestone by id' do
get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
+
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(milestone.title)
expect(json_response['iid']).to eq(milestone.iid)
@@ -60,11 +63,13 @@ describe API::API, api: true do
it 'returns 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}")
+
expect(response).to have_http_status(401)
end
it 'returns a 404 error if milestone id not found' do
get api("/projects/#{project.id}/milestones/1234", user)
+
expect(response).to have_http_status(404)
end
end
@@ -72,6 +77,7 @@ describe API::API, api: true do
describe 'POST /projects/:id/milestones' do
it 'creates a new project milestone' do
post api("/projects/#{project.id}/milestones", user), title: 'new milestone'
+
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new milestone')
expect(json_response['description']).to be_nil
@@ -80,6 +86,7 @@ describe API::API, api: true do
it 'creates a new project milestone with description and due date' do
post api("/projects/#{project.id}/milestones", user),
title: 'new milestone', description: 'release', due_date: '2013-03-02'
+
expect(response).to have_http_status(201)
expect(json_response['description']).to eq('release')
expect(json_response['due_date']).to eq('2013-03-02')
@@ -87,14 +94,31 @@ describe API::API, api: true do
it 'returns a 400 error if title is missing' do
post api("/projects/#{project.id}/milestones", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 400 error if params are invalid (duplicate title)' do
+ post api("/projects/#{project.id}/milestones", user),
+ title: milestone.title, description: 'release', due_date: '2013-03-02'
+
expect(response).to have_http_status(400)
end
+
+ it 'creates a new project with reserved html characters' do
+ post api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2')
+ expect(json_response['description']).to be_nil
+ end
end
describe 'PUT /projects/:id/milestones/:milestone_id' do
it 'updates a project milestone' do
put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
title: 'updated title'
+
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
@@ -102,6 +126,7 @@ describe API::API, api: true do
it 'returns a 404 error if milestone id not found' do
put api("/projects/#{project.id}/milestones/1234", user),
title: 'updated title'
+
expect(response).to have_http_status(404)
end
end
@@ -131,6 +156,7 @@ describe API::API, api: true do
end
it 'returns project issues for a particular milestone' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['milestone']['title']).to eq(milestone.title)
@@ -138,11 +164,12 @@ describe API::API, api: true do
it 'returns a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
+
expect(response).to have_http_status(401)
end
describe 'confidential issues' do
- let(:public_project) { create(:project, :public) }
+ let(:public_project) { create(:empty_project, :public) }
let(:milestone) { create(:milestone, project: public_project) }
let(:issue) { create(:issue, project: public_project) }
let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 223444ea39f..063a8706e76 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -220,6 +220,15 @@ describe API::API, api: true do
expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
end
end
+
+ context 'when the user is posting an award emoji' do
+ it 'returns an award emoji' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['awardable_id']).to eq issue.id
+ end
+ end
end
context "when noteable is a Snippet" do
diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb
new file mode 100644
index 00000000000..e6d8a5ee954
--- /dev/null
+++ b/spec/requests/api/notification_settings_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let!(:group) { create(:group) }
+ let!(:project) { create(:project, :public, creator_id: user.id, namespace: group) }
+
+ describe "GET /notification_settings" do
+ it "returns global notification settings for the current user" do
+ get api("/notification_settings", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_a Hash
+ expect(json_response['notification_email']).to eq(user.notification_email)
+ expect(json_response['level']).to eq(user.global_notification_setting.level)
+ end
+ end
+
+ describe "PUT /notification_settings" do
+ let(:email) { create(:email, user: user) }
+
+ it "updates global notification settings for the current user" do
+ put api("/notification_settings", user), { level: 'watch', notification_email: email.email }
+
+ expect(response).to have_http_status(200)
+ expect(json_response['notification_email']).to eq(email.email)
+ expect(user.reload.notification_email).to eq(email.email)
+ expect(json_response['level']).to eq(user.reload.global_notification_setting.level)
+ end
+ end
+
+ describe "PUT /notification_settings" do
+ it "fails on non-user email address" do
+ put api("/notification_settings", user), { notification_email: 'invalid@example.com' }
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe "GET /groups/:id/notification_settings" do
+ it "returns group level notification settings for the current user" do
+ get api("/groups/#{group.id}/notification_settings", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_a Hash
+ expect(json_response['level']).to eq(user.notification_settings_for(group).level)
+ end
+ end
+
+ describe "PUT /groups/:id/notification_settings" do
+ it "updates group level notification settings for the current user" do
+ put api("/groups/#{group.id}/notification_settings", user), { level: 'watch' }
+
+ expect(response).to have_http_status(200)
+ expect(json_response['level']).to eq(user.reload.notification_settings_for(group).level)
+ end
+ end
+
+ describe "GET /projects/:id/notification_settings" do
+ it "returns project level notification settings for the current user" do
+ get api("/projects/#{project.id}/notification_settings", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_a Hash
+ expect(json_response['level']).to eq(user.notification_settings_for(project).level)
+ end
+ end
+
+ describe "PUT /projects/:id/notification_settings" do
+ it "updates project level notification settings for the current user" do
+ put api("/projects/#{project.id}/notification_settings", user), { level: 'custom', new_note: true }
+
+ expect(response).to have_http_status(200)
+ expect(json_response['level']).to eq(user.reload.notification_settings_for(project).level)
+ expect(json_response['events']['new_note']).to eq(true)
+ expect(json_response['events']['new_issue']).to eq(false)
+ end
+ end
+
+ describe "PUT /projects/:id/notification_settings" do
+ it "fails on invalid level" do
+ put api("/projects/#{project.id}/notification_settings", user), { level: 'invalid' }
+
+ expect(response).to have_http_status(400)
+ end
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 28aa56e8644..4a0d727faea 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -225,7 +225,8 @@ describe API::API, api: true do
issues_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
- only_allow_merge_if_build_succeeds: false
+ only_allow_merge_if_build_succeeds: false,
+ request_access_enabled: true
})
post api('/projects', user), project
@@ -352,7 +353,8 @@ describe API::API, api: true do
description: FFaker::Lorem.sentence,
issues_enabled: false,
merge_requests_enabled: false,
- wiki_enabled: false
+ wiki_enabled: false,
+ request_access_enabled: true
})
post api("/projects/user/#{user.id}", admin), project
@@ -759,13 +761,16 @@ describe API::API, api: true do
let(:group) { create(:group) }
it "shares project with group" do
+ expires_at = 10.days.from_now.to_date
+
expect do
- post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
+ post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at
end.to change { ProjectGroupLink.count }.by(1)
expect(response.status).to eq 201
- expect(json_response['group_id']).to eq group.id
- expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER
+ expect(json_response['group_id']).to eq(group.id)
+ expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER)
+ expect(json_response['expires_at']).to eq(expires_at.to_s)
end
it "returns a 400 error when group id is not given" do
@@ -887,6 +892,15 @@ describe API::API, api: true do
expect(json_response['message']['name']).to eq(['has already been taken'])
end
+ it 'updates request_access_enabled' do
+ project_param = { request_access_enabled: false }
+
+ put api("/projects/#{project.id}", user), project_param
+
+ expect(response).to have_http_status(200)
+ expect(json_response['request_access_enabled']).to eq(false)
+ end
+
it 'updates path & name to existing path & name in different namespace' do
project_param = { path: project4.path, name: project4.name }
put api("/projects/#{project3.id}", user), project_param
@@ -948,7 +962,8 @@ describe API::API, api: true do
wiki_enabled: true,
snippets_enabled: true,
merge_requests_enabled: true,
- description: 'new description' }
+ description: 'new description',
+ request_access_enabled: true }
put api("/projects/#{project.id}", user3), project_param
expect(response).to have_http_status(403)
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 54d096e8b7f..f4903d8e0be 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -14,22 +14,38 @@ describe API::API, 'Settings', api: true do
expect(json_response['default_projects_limit']).to eq(42)
expect(json_response['signin_enabled']).to be_truthy
expect(json_response['repository_storage']).to eq('default')
+ expect(json_response['koding_enabled']).to be_falsey
+ expect(json_response['koding_url']).to be_nil
end
end
describe "PUT /application/settings" do
- before do
- storages = { 'custom' => 'tmp/tests/custom_repositories' }
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ context "custom repository storage type set in the config" do
+ before do
+ storages = { 'custom' => 'tmp/tests/custom_repositories' }
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ end
+
+ it "updates application settings" do
+ put api("/application/settings", admin),
+ default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com'
+ expect(response).to have_http_status(200)
+ expect(json_response['default_projects_limit']).to eq(3)
+ expect(json_response['signin_enabled']).to be_falsey
+ expect(json_response['repository_storage']).to eq('custom')
+ expect(json_response['koding_enabled']).to be_truthy
+ expect(json_response['koding_url']).to eq('http://koding.example.com')
+ end
end
- it "updates application settings" do
- put api("/application/settings", admin),
- default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom'
- expect(response).to have_http_status(200)
- expect(json_response['default_projects_limit']).to eq(3)
- expect(json_response['signin_enabled']).to be_falsey
- expect(json_response['repository_storage']).to eq('custom')
+ context "missing koding_url value when koding_enabled is true" do
+ it "returns a blank parameter error message" do
+ put api("/application/settings", admin), koding_enabled: true
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to have_key('koding_url')
+ expect(json_response['message']['koding_url']).to include "can't be blank"
+ end
end
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index ef73778efa9..f4ea3bebb4c 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -62,6 +62,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first.keys).to include 'email'
+ expect(json_response.first.keys).to include 'organization'
expect(json_response.first.keys).to include 'identities'
expect(json_response.first.keys).to include 'can_create_project'
expect(json_response.first.keys).to include 'two_factor_enabled'
@@ -265,6 +266,14 @@ describe API::API, api: true do
expect(user.reload.bio).to eq('new test bio')
end
+ it "updates user with organization" do
+ put api("/users/#{user.id}", admin), { organization: 'GitLab' }
+
+ expect(response).to have_http_status(200)
+ expect(json_response['organization']).to eq('GitLab')
+ expect(user.reload.organization).to eq('GitLab')
+ end
+
it 'updates user with his own email' do
put api("/users/#{user.id}", admin), email: user.email
expect(response).to have_http_status(200)
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index ca7932dc5da..df97f1bf7b6 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -15,6 +15,25 @@ describe Ci::API::API do
describe "POST /builds/register" do
let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' }
+
+ shared_examples 'no builds available' do
+ context 'when runner sends version in User-Agent' do
+ context 'for stable version' do
+ it { expect(response).to have_http_status(204) }
+ end
+
+ context 'for beta version' do
+ let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (1-5-stable; go1.6.3; linux/amd64)' }
+ it { expect(response).to have_http_status(204) }
+ end
+ end
+
+ context "when runner doesn't send version in User-Agent" do
+ let(:user_agent) { 'Go-http-client/1.1' }
+ it { expect(response).to have_http_status(404) }
+ end
+ end
it "starts a build" do
register_builds info: { platform: :darwin }
@@ -33,36 +52,30 @@ describe Ci::API::API do
context 'when builds are finished' do
before do
build.success
- end
-
- it "returns 404 error if no builds for specific runner" do
register_builds
-
- expect(response).to have_http_status(404)
end
+
+ it_behaves_like 'no builds available'
end
context 'for other project with builds' do
before do
build.success
create(:ci_build, :pending)
- end
-
- it "returns 404 error if no builds for shared runner" do
register_builds
-
- expect(response).to have_http_status(404)
end
+
+ it_behaves_like 'no builds available'
end
context 'for shared runner' do
let(:shared_runner) { create(:ci_runner, token: "SharedRunner") }
- it "should return 404 error if no builds for shared runner" do
+ before do
register_builds shared_runner.token
-
- expect(response).to have_http_status(404)
end
+
+ it_behaves_like 'no builds available'
end
context 'for triggered build' do
@@ -136,18 +149,27 @@ describe Ci::API::API do
end
context 'when runner is not allowed to pick untagged builds' do
- before { runner.update_column(:run_untagged, false) }
-
- it 'does not pick build' do
+ before do
+ runner.update_column(:run_untagged, false)
register_builds
-
- expect(response).to have_http_status 404
end
+
+ it_behaves_like 'no builds available'
+ end
+ end
+
+ context 'when runner is paused' do
+ let(:inactive_runner) { create(:ci_runner, :inactive, token: "InactiveRunner") }
+
+ before do
+ register_builds inactive_runner.token
end
+
+ it { expect(response).to have_http_status 404 }
end
def register_builds(token = runner.token, **params)
- post ci_api("/builds/register"), params.merge(token: token)
+ post ci_api("/builds/register"), params.merge(token: token), { 'User-Agent' => user_agent }
end
end
@@ -230,8 +252,10 @@ describe Ci::API::API do
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.token) }
+ let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
+ let(:headers) { { "GitLab-Workhorse" => "1.0", Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
+ let(:token) { build.token }
+ let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => token) }
before { build.run! }
@@ -239,27 +263,51 @@ describe Ci::API::API do
context "should authorize posting artifact to running build" do
it "using token as parameter" do
post authorize_url, { token: build.token }, headers
+
expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response["TempPath"]).not_to be_nil
end
it "using token as header" do
post authorize_url, {}, headers_with_token
+
expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response["TempPath"]).not_to be_nil
end
+
+ it "using runners token" do
+ post authorize_url, { token: build.project.runners_token }, headers
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response["TempPath"]).not_to be_nil
+ end
+
+ it "reject requests that did not go through gitlab-workhorse" do
+ headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
+
+ post authorize_url, { token: build.token }, headers
+
+ expect(response).to have_http_status(500)
+ end
end
context "should fail to post too large artifact" do
it "using token as parameter" do
stub_application_setting(max_artifacts_size: 0)
+
post authorize_url, { token: build.token, filesize: 100 }, headers
+
expect(response).to have_http_status(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).to have_http_status(413)
end
end
@@ -327,6 +375,16 @@ describe Ci::API::API do
it_behaves_like 'successful artifacts upload'
end
+
+ context 'when using runners token' do
+ let(:token) { build.project.runners_token }
+
+ before do
+ upload_artifacts(file_upload, headers_with_token)
+ end
+
+ it_behaves_like 'successful artifacts upload'
+ end
end
context 'posts artifacts file and metadata file' do
@@ -466,19 +524,40 @@ describe Ci::API::API do
before do
delete delete_url, token: build.token
- build.reload
end
- it 'removes build artifacts' do
- expect(response).to have_http_status(200)
- expect(build.artifacts_file.exists?).to be_falsy
- expect(build.artifacts_metadata.exists?).to be_falsy
- expect(build.artifacts_size).to be_nil
+ shared_examples 'having removable artifacts' do
+ it 'removes build artifacts' do
+ build.reload
+
+ expect(response).to have_http_status(200)
+ expect(build.artifacts_file.exists?).to be_falsy
+ expect(build.artifacts_metadata.exists?).to be_falsy
+ expect(build.artifacts_size).to be_nil
+ end
+ end
+
+ context 'when using build token' do
+ before do
+ delete delete_url, token: build.token
+ end
+
+ it_behaves_like 'having removable artifacts'
+ end
+
+ context 'when using runnners token' do
+ before do
+ delete delete_url, token: build.project.runners_token
+ end
+
+ it_behaves_like 'having removable artifacts'
end
end
describe 'GET /builds/:id/artifacts' do
- before { get get_url, token: build.token }
+ before do
+ get get_url, token: token
+ end
context 'build has artifacts' do
let(:build) { create(:ci_build, :artifacts) }
@@ -487,13 +566,29 @@ describe Ci::API::API do
'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
end
- it 'downloads artifact' do
- expect(response).to have_http_status(200)
- expect(response.headers).to include download_headers
+ shared_examples 'having downloadable artifacts' do
+ it 'download artifacts' do
+ expect(response).to have_http_status(200)
+ expect(response.headers).to include download_headers
+ end
+ end
+
+ context 'when using build token' do
+ let(:token) { build.token }
+
+ it_behaves_like 'having downloadable artifacts'
+ end
+
+ context 'when using runnners token' do
+ let(:token) { build.project.runners_token }
+
+ it_behaves_like 'having downloadable artifacts'
end
end
context 'build does not has artifacts' do
+ let(:token) { build.token }
+
it 'responds with not found' do
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 9ca3b021aa2..c0c1e62e910 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -1,8 +1,8 @@
require "spec_helper"
describe 'Git HTTP requests', lib: true do
- let(:user) { create(:user) }
- let(:project) { create(:project, path: 'project.git-project') }
+ include GitHttpHelpers
+ include WorkhorseHelpers
it "gives WWW-Authenticate hints" do
clone_get('doesnt/exist.git')
@@ -10,429 +10,508 @@ describe 'Git HTTP requests', lib: true do
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
- context "when the project doesn't exist" do
- context "when no authentication is provided" do
- it "responds with status 401 (no project existence information leak)" do
- download('doesnt/exist.git') do |response|
- expect(response).to have_http_status(401)
- end
- end
- end
+ describe "User with no identities" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, path: 'project.git-project') }
- context "when username and password are provided" do
- context "when authentication fails" do
- it "responds with status 401" do
- download('doesnt/exist.git', user: user.username, password: "nope") do |response|
+ context "when the project doesn't exist" do
+ context "when no authentication is provided" do
+ it "responds with status 401 (no project existence information leak)" do
+ download('doesnt/exist.git') do |response|
expect(response).to have_http_status(401)
end
end
end
- context "when authentication succeeds" do
- it "responds with status 404" do
- download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(404)
+ context "when username and password are provided" do
+ context "when authentication fails" do
+ it "responds with status 401" do
+ download('doesnt/exist.git', user: user.username, password: "nope") do |response|
+ expect(response).to have_http_status(401)
+ end
end
end
- end
- end
- end
-
- context "when the Wiki for a project exists" do
- it "responds with the right project" do
- wiki = ProjectWiki.new(project)
- project.update_attribute(:visibility_level, Project::PUBLIC)
- download("/#{wiki.repository.path_with_namespace}.git") do |response|
- json_body = ActiveSupport::JSON.decode(response.body)
-
- expect(response).to have_http_status(200)
- expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
+ context "when authentication succeeds" do
+ it "responds with status 404" do
+ download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
end
- end
- context "when the project exists" do
- let(:path) { "#{project.path_with_namespace}.git" }
-
- context "when the project is public" do
- before do
+ context "when the Wiki for a project exists" do
+ it "responds with the right project" do
+ wiki = ProjectWiki.new(project)
project.update_attribute(:visibility_level, Project::PUBLIC)
- end
- it "downloads get status 200" do
- download(path, {}) do |response|
+ download("/#{wiki.repository.path_with_namespace}.git") do |response|
+ json_body = ActiveSupport::JSON.decode(response.body)
+
expect(response).to have_http_status(200)
+ expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
+ end
+
+ context "when the project exists" do
+ let(:path) { "#{project.path_with_namespace}.git" }
- it "uploads get status 401" do
- upload(path, {}) do |response|
- expect(response).to have_http_status(401)
+ context "when the project is public" do
+ before do
+ project.update_attribute(:visibility_level, Project::PUBLIC)
end
- end
- context "with correct credentials" do
- let(:env) { { user: user.username, password: user.password } }
+ it "downloads get status 200" do
+ download(path, {}) do |response|
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+ end
- it "uploads get status 403" do
- upload(path, env) do |response|
- expect(response).to have_http_status(403)
+ it "uploads get status 401" do
+ upload(path, {}) do |response|
+ expect(response).to have_http_status(401)
end
end
- context 'but git-receive-pack is disabled' do
- it "responds with status 404" do
- allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
+ context "with correct credentials" do
+ let(:env) { { user: user.username, password: user.password } }
+ it "uploads get status 403" do
upload(path, env) do |response|
expect(response).to have_http_status(403)
end
end
- end
- end
- context 'but git-upload-pack is disabled' do
- it "responds with status 404" do
- allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
+ context 'but git-receive-pack is disabled' do
+ it "responds with status 404" do
+ allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
- download(path, {}) do |response|
- expect(response).to have_http_status(404)
+ upload(path, env) do |response|
+ expect(response).to have_http_status(403)
+ end
+ end
end
end
- end
- end
- context "when the project is private" do
- before do
- project.update_attribute(:visibility_level, Project::PRIVATE)
- end
+ context 'but git-upload-pack is disabled' do
+ it "responds with status 404" do
+ allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
- context "when no authentication is provided" do
- it "responds with status 401 to downloads" do
- download(path, {}) do |response|
- expect(response).to have_http_status(401)
+ download(path, {}) do |response|
+ expect(response).to have_http_status(404)
+ end
end
end
- it "responds with status 401 to uploads" do
- upload(path, {}) do |response|
- expect(response).to have_http_status(401)
+ context 'when the request is not from gitlab-workhorse' do
+ it 'raises an exception' do
+ expect do
+ get("/#{project.path_with_namespace}.git/info/refs?service=git-upload-pack")
+ end.to raise_error(JWT::DecodeError)
end
end
end
- context "when username and password are provided" do
- let(:env) { { user: user.username, password: 'nope' } }
+ context "when the project is private" do
+ before do
+ project.update_attribute(:visibility_level, Project::PRIVATE)
+ end
- context "when authentication fails" do
- it "responds with status 401" do
- download(path, env) do |response|
+ context "when no authentication is provided" do
+ it "responds with status 401 to downloads" do
+ download(path, {}) do |response|
expect(response).to have_http_status(401)
end
end
- context "when the user is IP banned" do
- it "responds with status 401" do
- expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
- allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
-
- clone_get(path, env)
-
+ it "responds with status 401 to uploads" do
+ upload(path, {}) do |response|
expect(response).to have_http_status(401)
end
end
end
- context "when authentication succeeds" do
- let(:env) { { user: user.username, password: user.password } }
+ context "when username and password are provided" do
+ let(:env) { { user: user.username, password: 'nope' } }
- context "when the user has access to the project" do
- before do
- project.team << [user, :master]
+ context "when authentication fails" do
+ it "responds with status 401" do
+ download(path, env) do |response|
+ expect(response).to have_http_status(401)
+ end
end
- context "when the user is blocked" do
- it "responds with status 404" do
- user.block
- project.team << [user, :master]
+ context "when the user is IP banned" do
+ it "responds with status 401" do
+ expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
- download(path, env) do |response|
- expect(response).to have_http_status(404)
- end
+ clone_get(path, env)
+
+ expect(response).to have_http_status(401)
end
end
+ end
- context "when the user isn't blocked" do
- it "downloads get status 200" do
- expect(Rack::Attack::Allow2Ban).to receive(:reset)
+ context "when authentication succeeds" do
+ let(:env) { { user: user.username, password: user.password } }
- clone_get(path, env)
+ context "when the user has access to the project" do
+ before do
+ project.team << [user, :master]
+ end
- expect(response).to have_http_status(200)
+ context "when the user is blocked" do
+ it "responds with status 404" do
+ user.block
+ project.team << [user, :master]
+
+ download(path, env) do |response|
+ expect(response).to have_http_status(404)
+ end
+ end
end
- it "uploads get status 200" do
- upload(path, env) do |response|
+ context "when the user isn't blocked" do
+ it "downloads get status 200" do
+ expect(Rack::Attack::Allow2Ban).to receive(:reset)
+
+ clone_get(path, env)
+
expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
- end
- end
- context "when an oauth token is provided" do
- before do
- application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
- @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
+ it "uploads get status 200" do
+ upload(path, env) do |response|
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+ end
end
- it "downloads get status 200" do
- clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+ context "when an oauth token is provided" do
+ before do
+ application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
+ @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
+ end
- expect(response).to have_http_status(200)
- end
+ it "downloads get status 200" do
+ clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
- it "uploads get status 401 (no project existence information leak)" do
- push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
- expect(response).to have_http_status(401)
+ it "uploads get status 401 (no project existence information leak)" do
+ push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+
+ expect(response).to have_http_status(401)
+ end
end
- end
- context 'when user has 2FA enabled' do
- let(:user) { create(:user, :two_factor) }
- let(:access_token) { create(:personal_access_token, user: user) }
+ context 'when user has 2FA enabled' do
+ let(:user) { create(:user, :two_factor) }
+ let(:access_token) { create(:personal_access_token, user: user) }
- before do
- project.team << [user, :master]
- end
+ before do
+ project.team << [user, :master]
+ end
+
+ context 'when username and password are provided' do
+ it 'rejects the clone attempt' do
+ download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ end
+ end
- context 'when username and password are provided' do
- it 'rejects the clone attempt' do
- download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(401)
- expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ it 'rejects the push attempt' do
+ upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ end
end
end
- it 'rejects the push attempt' do
- upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(401)
- expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ context 'when username and personal access token are provided' do
+ it 'allows clones' do
+ download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ it 'allows pushes' do
+ upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
+ expect(response).to have_http_status(200)
+ end
end
end
end
- context 'when username and personal access token are provided' do
- it 'allows clones' do
- download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
- expect(response).to have_http_status(200)
- end
+ context "when blank password attempts follow a valid login" do
+ def attempt_login(include_password)
+ password = include_password ? user.password : ""
+ clone_get path, user: user.username, password: password
+ response.status
end
- it 'allows pushes' do
- upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
- expect(response).to have_http_status(200)
+ it "repeated attempts followed by successful attempt" do
+ options = Gitlab.config.rack_attack.git_basic_auth
+ maxretry = options[:maxretry] - 1
+ ip = '1.2.3.4'
+
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
+ Rack::Attack::Allow2Ban.reset(ip, options)
+
+ 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
+
+ maxretry.times.each do
+ expect(attempt_login(false)).to eq(401)
end
+
+ Rack::Attack::Allow2Ban.reset(ip, options)
end
end
end
- context "when blank password attempts follow a valid login" do
- def attempt_login(include_password)
- password = include_password ? user.password : ""
- clone_get path, user: user.username, password: password
- response.status
+ context "when the user doesn't have access to the project" do
+ it "downloads get status 404" do
+ download(path, user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ it "uploads get status 404" do
+ upload(path, user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(404)
+ end
end
+ end
+ end
+ end
- it "repeated attempts followed by successful attempt" do
- options = Gitlab.config.rack_attack.git_basic_auth
- maxretry = options[:maxretry] - 1
- ip = '1.2.3.4'
+ context "when a gitlab ci token is provided" do
+ let(:build) { create(:ci_build, :running) }
+ let(:project) { build.project }
+ let(:other_project) { create(:empty_project) }
- allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
- Rack::Attack::Allow2Ban.reset(ip, options)
+ before do
+ project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
+ end
- maxretry.times.each do
- expect(attempt_login(false)).to eq(401)
- end
+ context 'when build created by system is authenticated' do
+ it "downloads get status 200" do
+ clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
- expect(attempt_login(true)).to eq(200)
- expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
+ it "uploads get status 401 (no project existence information leak)" do
+ push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
- maxretry.times.each do
- expect(attempt_login(false)).to eq(401)
- end
+ expect(response).to have_http_status(401)
+ end
- Rack::Attack::Allow2Ban.reset(ip, options)
- end
+ it "downloads from other project get status 404" do
+ clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(404)
end
end
- context "when the user doesn't have access to the project" do
- it "downloads get status 404" do
- download(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(404)
- end
+ context 'and build created by' do
+ before do
+ build.update(user: user)
+ project.team << [user, :reporter]
end
- it "uploads get status 404" do
- upload(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(404)
+ shared_examples 'can download code only' do
+ it 'downloads get status 200' do
+ clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+
+ it 'uploads get status 403' do
+ push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(401)
end
end
- end
- end
- end
- context "when a gitlab ci token is provided" do
- let(:token) { 123 }
- let(:project) { FactoryGirl.create :empty_project }
+ context 'administrator' do
+ let(:user) { create(:admin) }
- before do
- project.update_attributes(runners_token: token)
- project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
- end
+ it_behaves_like 'can download code only'
- it "downloads get status 200" do
- clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
+ it 'downloads from other project get status 403' do
+ clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_http_status(200)
- end
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'regular user' do
+ let(:user) { create(:user) }
+
+ it_behaves_like 'can download code only'
- it "uploads get status 401 (no project existence information leak)" do
- push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
+ it 'downloads from other project get status 404' do
+ clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_http_status(401)
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
end
end
- end
- context "when the project path doesn't end in .git" do
- context "GET info/refs" do
- let(:path) { "/#{project.path_with_namespace}/info/refs" }
+ context "when the project path doesn't end in .git" do
+ context "GET info/refs" do
+ let(:path) { "/#{project.path_with_namespace}/info/refs" }
- context "when no params are added" do
- before { get path }
+ context "when no params are added" do
+ before { get path }
- it "redirects to the .git suffix version" do
- expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
+ end
end
- end
- context "when the upload-pack service is requested" do
- let(:params) { { service: 'git-upload-pack' } }
- before { get path, params }
+ context "when the upload-pack service is requested" do
+ let(:params) { { service: 'git-upload-pack' } }
+ before { get path, params }
- it "redirects to the .git suffix version" do
- expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+ end
end
- end
- context "when the receive-pack service is requested" do
- let(:params) { { service: 'git-receive-pack' } }
- before { get path, params }
+ context "when the receive-pack service is requested" do
+ let(:params) { { service: 'git-receive-pack' } }
+ before { get path, params }
- it "redirects to the .git suffix version" do
- expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+ end
end
- end
- context "when the params are anything else" do
- let(:params) { { service: 'git-implode-pack' } }
- before { get path, params }
+ context "when the params are anything else" do
+ let(:params) { { service: 'git-implode-pack' } }
+ before { get path, params }
- it "redirects to the sign-in page" do
- expect(response).to redirect_to(new_user_session_path)
+ it "redirects to the sign-in page" do
+ expect(response).to redirect_to(new_user_session_path)
+ end
end
end
- end
- context "POST git-upload-pack" do
- it "fails to find a route" do
- expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+ context "POST git-upload-pack" do
+ it "fails to find a route" do
+ expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+ end
end
- end
- context "POST git-receive-pack" do
- it "failes to find a route" do
- expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+ context "POST git-receive-pack" do
+ it "failes to find a route" do
+ expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+ end
end
end
- end
- context "retrieving an info/refs file" do
- before { project.update_attribute(:visibility_level, Project::PUBLIC) }
+ context "retrieving an info/refs file" do
+ before { project.update_attribute(:visibility_level, Project::PUBLIC) }
- context "when the file exists" do
- before do
- # Provide a dummy file in its place
- allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
- allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do
- Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore')
- end
+ context "when the file exists" do
+ before do
+ # Provide a dummy file in its place
+ allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
+ allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do
+ Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore')
+ end
- get "/#{project.path_with_namespace}/blob/master/info/refs"
- end
+ get "/#{project.path_with_namespace}/blob/master/info/refs"
+ end
- it "returns the file" do
- expect(response).to have_http_status(200)
+ it "returns the file" do
+ expect(response).to have_http_status(200)
+ end
end
- end
- context "when the file does not exist" do
- before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
+ context "when the file does not exist" do
+ before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
- it "returns not found" do
- expect(response).to have_http_status(404)
+ it "returns not found" do
+ expect(response).to have_http_status(404)
+ end
end
end
end
- def clone_get(project, options = {})
- get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
- end
-
- def clone_post(project, options = {})
- post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
- end
-
- def push_get(project, options = {})
- get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
- end
+ describe "User with LDAP identity" do
+ let(:user) { create(:omniauth_user, extern_uid: dn) }
+ let(:dn) { 'uid=john,ou=people,dc=example,dc=com' }
- def push_post(project, options = {})
- post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
- end
-
- def download(project, user: nil, password: nil, spnego_request_token: nil)
- args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+ before do
+ allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
+ allow(Gitlab::LDAP::Authentication).to receive(:login).and_return(nil)
+ allow(Gitlab::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user)
+ end
- clone_get(*args)
- yield response
+ context "when authentication fails" do
+ context "when no authentication is provided" do
+ it "responds with status 401" do
+ download('doesnt/exist.git') do |response|
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
- clone_post(*args)
- yield response
- end
+ context "when username and invalid password are provided" do
+ it "responds with status 401" do
+ download('doesnt/exist.git', user: user.username, password: "nope") do |response|
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+ end
- def upload(project, user: nil, password: nil, spnego_request_token: nil)
- args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+ context "when authentication succeeds" do
+ context "when the project doesn't exist" do
+ it "responds with status 404" do
+ download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
- push_get(*args)
- yield response
+ context "when the project exists" do
+ let(:project) { create(:project, path: 'project.git-project') }
- push_post(*args)
- yield response
- end
+ before do
+ project.team << [user, :master]
+ end
- def auth_env(user, password, spnego_request_token)
- env = {}
- if user && password
- env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
- elsif spnego_request_token
- env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
+ it "responds with status 200" do
+ clone_get(path, user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
end
-
- env
end
end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index fc42b534dca..f0ef155bd7b 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -22,11 +22,13 @@ describe JwtController do
context 'when using authorized request' do
context 'using CI token' do
- let(:project) { create(:empty_project, runners_token: 'token') }
- let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } }
+ let(:build) { create(:ci_build, :running) }
+ let(:project) { build.project }
+ let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } }
context 'project with enabled CI' do
subject! { get '/jwt/auth', parameters, headers }
+
it { expect(service_class).to have_received(:new).with(project, nil, parameters) }
end
@@ -37,19 +39,37 @@ describe JwtController do
subject! { get '/jwt/auth', parameters, headers }
- it { expect(response).to have_http_status(403) }
+ it { expect(response).to have_http_status(401) }
end
end
context 'using User login' do
let(:user) { create(:user) }
- let(:headers) { { authorization: credentials('user', 'password') } }
-
- before { expect(Gitlab::Auth).to receive(:find_with_user_password).with('user', 'password').and_return(user) }
+ let(:headers) { { authorization: credentials(user.username, user.password) } }
subject! { get '/jwt/auth', parameters, headers }
it { expect(service_class).to have_received(:new).with(nil, user, parameters) }
+
+ context 'when user has 2FA enabled' do
+ let(:user) { create(:user, :two_factor) }
+
+ context 'without personal token' do
+ it 'rejects the authorization attempt' do
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ end
+ end
+
+ context 'with personal token' do
+ let(:access_token) { create(:personal_access_token, user: user) }
+ let(:headers) { { authorization: credentials(user.username, access_token.token) } }
+
+ it 'rejects the authorization attempt' do
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
end
context 'using invalid login' do
@@ -57,7 +77,7 @@ describe JwtController do
subject! { get '/jwt/auth', parameters, headers }
- it { expect(response).to have_http_status(403) }
+ it { expect(response).to have_http_status(401) }
end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index fcd6521317a..dbdf83a0dff 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Git LFS API and storage' do
+ include WorkhorseHelpers
+
let(:user) { create(:user) }
let!(:lfs_object) { create(:lfs_object, :with_file) }
@@ -12,6 +14,7 @@ describe 'Git LFS API and storage' do
end
let(:authorization) { }
let(:sendfile) { }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:sample_oid) { lfs_object.oid }
let(:sample_size) { lfs_object.size }
@@ -242,15 +245,99 @@ describe 'Git LFS API and storage' do
end
end
- context 'when CI is authorized' do
- let(:authorization) { authorize_ci_project }
+ context 'when deploy key is authorized' do
+ let(:key) { create(:deploy_key) }
+ let(:authorization) { authorize_deploy_key }
let(:update_permissions) do
+ project.deploy_keys << key
project.lfs_objects << lfs_object
end
it_behaves_like 'responds with a file'
end
+
+ describe 'when using a user key' do
+ let(:authorization) { authorize_user_key }
+
+ context 'when user allowed' do
+ let(:update_permissions) do
+ project.team << [user, :master]
+ project.lfs_objects << lfs_object
+ end
+
+ it_behaves_like 'responds with a file'
+ end
+
+ context 'when user not allowed' do
+ let(:update_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when build is authorized as' do
+ let(:authorization) { authorize_ci_project }
+
+ shared_examples 'can download LFS only from own projects' do
+ context 'for own project' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:update_permissions) do
+ project.team << [user, :reporter]
+ project.lfs_objects << lfs_object
+ end
+
+ it_behaves_like 'responds with a file'
+ end
+
+ context 'for other project' do
+ let(:other_project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+
+ let(:update_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'rejects downloading code' do
+ expect(response).to have_http_status(other_project_status)
+ end
+ end
+ end
+
+ context 'administrator' do
+ let(:user) { create(:admin) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it_behaves_like 'can download LFS only from own projects' do
+ # We render 403, because administrator does have normally access
+ let(:other_project_status) { 403 }
+ end
+ end
+
+ context 'regular user' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it_behaves_like 'can download LFS only from own projects' do
+ # We render 404, to prevent data leakage about existence of the project
+ let(:other_project_status) { 404 }
+ end
+ end
+
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ it_behaves_like 'can download LFS only from own projects' do
+ # We render 404, to prevent data leakage about existence of the project
+ let(:other_project_status) { 404 }
+ end
+ end
+ end
end
context 'without required headers' do
@@ -429,10 +516,62 @@ describe 'Git LFS API and storage' do
end
end
- context 'when CI is authorized' do
+ context 'when build is authorized as' do
let(:authorization) { authorize_ci_project }
- it_behaves_like 'an authorized requests'
+ let(:update_lfs_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ shared_examples 'can download LFS only from own projects' do
+ context 'for own project' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:update_user_permissions) do
+ project.team << [user, :reporter]
+ end
+
+ it_behaves_like 'an authorized requests'
+ end
+
+ context 'for other project' do
+ let(:other_project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+
+ it 'rejects downloading code' do
+ expect(response).to have_http_status(other_project_status)
+ end
+ end
+ end
+
+ context 'administrator' do
+ let(:user) { create(:admin) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it_behaves_like 'can download LFS only from own projects' do
+ # We render 403, because administrator does have normally access
+ let(:other_project_status) { 403 }
+ end
+ end
+
+ context 'regular user' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it_behaves_like 'can download LFS only from own projects' do
+ # We render 404, to prevent data leakage about existence of the project
+ let(:other_project_status) { 404 }
+ end
+ end
+
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ it_behaves_like 'can download LFS only from own projects' do
+ # We render 404, to prevent data leakage about existence of the project
+ let(:other_project_status) { 404 }
+ end
+ end
end
context 'when user is not authenticated' do
@@ -581,11 +720,37 @@ describe 'Git LFS API and storage' do
end
end
- context 'when CI is authorized' do
+ context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
- it 'responds with 401' do
- expect(response).to have_http_status(401)
+ context 'build has an user' do
+ let(:user) { create(:user) }
+
+ context 'tries to push to own project' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'tries to push to other project' do
+ let(:other_project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
end
end
end
@@ -607,14 +772,6 @@ describe 'Git LFS API and storage' do
end
end
end
-
- context 'when CI is authorized' do
- let(:authorization) { authorize_ci_project }
-
- it 'responds with status 403' do
- expect(response).to have_http_status(401)
- end
- end
end
describe 'unsupported' do
@@ -715,6 +872,12 @@ describe 'Git LFS API and storage' do
project.team << [user, :developer]
end
+ context 'and the request bypassed workhorse' do
+ it 'raises an exception' do
+ expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError
+ end
+ end
+
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
put_authorize
@@ -724,6 +887,10 @@ describe 'Git LFS API and storage' do
expect(response).to have_http_status(200)
end
+ it 'uses the gitlab-workhorse content type' do
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+
it 'responds with status 200, location of lfs store and object details' do
expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
expect(json_response['LfsOid']).to eq(sample_oid)
@@ -767,10 +934,51 @@ describe 'Git LFS API and storage' do
end
end
- context 'when CI is authenticated' do
+ context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
- it_behaves_like 'unauthorized'
+ context 'build has an user' do
+ let(:user) { create(:user) }
+
+ context 'tries to push to own project' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ before do
+ project.team << [user, :developer]
+ put_authorize
+ end
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'tries to push to other project' do
+ let(:other_project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ before do
+ put_authorize
+ end
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ before do
+ put_authorize
+ end
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
end
context 'for unauthenticated' do
@@ -827,10 +1035,42 @@ describe 'Git LFS API and storage' do
end
end
- context 'when CI is authenticated' do
+ context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
- it_behaves_like 'unauthorized'
+ before do
+ put_authorize
+ end
+
+ context 'build has an user' do
+ let(:user) { create(:user) }
+
+ context 'tries to push to own project' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'tries to push to other project' do
+ let(:other_project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
end
context 'for unauthenticated' do
@@ -863,8 +1103,11 @@ describe 'Git LFS API and storage' do
end
end
- def put_authorize
- put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, headers
+ def put_authorize(verified: true)
+ authorize_headers = headers
+ authorize_headers.merge!(workhorse_internal_api_request_header) if verified
+
+ put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, authorize_headers
end
def put_finalize(lfs_tmp = lfs_tmp_file)
@@ -882,13 +1125,21 @@ describe 'Git LFS API and storage' do
end
def authorize_ci_project
- ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', project.runners_token)
+ ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', build.token)
end
def authorize_user
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end
+ def authorize_deploy_key
+ ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).token)
+ end
+
+ def authorize_user_key
+ ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token)
+ end
+
def fork_project(project, user, object = nil)
allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
Projects::ForkService.new(project, user, {}).execute
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 7cc71f706ce..c64df4979b0 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -6,8 +6,14 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
let(:current_params) { {} }
let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
let(:payload) { JWT.decode(subject[:token], rsa_key).first }
+ let(:authentication_abilities) do
+ [
+ :read_container_image,
+ :create_container_image
+ ]
+ end
- subject { described_class.new(current_project, current_user, current_params).execute }
+ subject { described_class.new(current_project, current_user, current_params).execute(authentication_abilities: authentication_abilities) }
before do
allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil)
@@ -189,13 +195,22 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
end
- context 'project authorization' do
+ context 'build authorized as user' do
let(:current_project) { create(:empty_project) }
+ let(:current_user) { create(:user) }
+ let(:authentication_abilities) do
+ [
+ :build_read_container_image,
+ :build_create_container_image
+ ]
+ end
- context 'allow to use scope-less authentication' do
- it_behaves_like 'a valid token'
+ before do
+ current_project.team << [current_user, :developer]
end
+ it_behaves_like 'a valid token'
+
context 'allow to pull and push images' do
let(:current_params) do
{ scope: "repository:#{current_project.path_with_namespace}:pull,push" }
@@ -214,12 +229,44 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
context 'allow for public' do
let(:project) { create(:empty_project, :public) }
+
it_behaves_like 'a pullable'
end
- context 'disallow for private' do
+ shared_examples 'pullable for being team member' do
+ context 'when you are not member' do
+ it_behaves_like 'an inaccessible'
+ end
+
+ context 'when you are member' do
+ before do
+ project.team << [current_user, :developer]
+ end
+
+ it_behaves_like 'a pullable'
+ end
+ end
+
+ context 'for private' do
let(:project) { create(:empty_project, :private) }
- it_behaves_like 'an inaccessible'
+
+ it_behaves_like 'pullable for being team member'
+
+ context 'when you are admin' do
+ let(:current_user) { create(:admin) }
+
+ context 'when you are not member' do
+ it_behaves_like 'an inaccessible'
+ end
+
+ context 'when you are member' do
+ before do
+ project.team << [current_user, :developer]
+ end
+
+ it_behaves_like 'a pullable'
+ end
+ end
end
end
@@ -230,6 +277,11 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
context 'disallow for all' do
let(:project) { create(:empty_project, :public) }
+
+ before do
+ project.team << [current_user, :developer]
+ end
+
it_behaves_like 'an inaccessible'
end
end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index cf4c5f13635..e65da15aca8 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -13,10 +13,10 @@ describe Boards::Issues::ListService, services: true do
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
- let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:backlog) { project.board.backlog_list }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
- let!(:done) { create(:done_list, board: board) }
+ let!(:done) { project.board.done_list }
let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) }
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 0122159cab8..180f1b08631 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -10,10 +10,10 @@ describe Boards::Issues::MoveService, services: true do
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
- let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:backlog) { project.board.backlog_list }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
- let!(:done) { create(:done_list, board: board) }
+ let!(:done) { project.board.done_list }
before do
project.team << [user, :developer]
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index 90764b86b16..bff9c1fd1fe 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -17,17 +17,15 @@ describe Boards::Lists::CreateService, services: true do
end
end
- context 'when board lists has only a backlog list' do
+ context 'when board lists has backlog, and done lists' do
it 'creates a new list at beginning of the list' do
- create(:backlog_list, board: board)
-
list = service.execute
expect(list.position).to eq 0
end
end
- context 'when board lists has only labels lists' do
+ context 'when board lists has labels lists' do
it 'creates a new list at end of the lists' do
create(:list, board: board, position: 0)
create(:list, board: board, position: 1)
@@ -40,8 +38,6 @@ describe Boards::Lists::CreateService, services: true do
context 'when board lists has backlog, label and done lists' do
it 'creates a new list at end of the label lists' do
- create(:backlog_list, board: board)
- create(:done_list, board: board)
list1 = create(:list, board: board, position: 0)
list2 = service.execute
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
index 6eff445feee..474c4512471 100644
--- a/spec/services/boards/lists/destroy_service_spec.rb
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -15,11 +15,11 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'decrements position of higher lists' do
- backlog = create(:backlog_list, board: board)
+ backlog = project.board.backlog_list
development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2)
- done = create(:done_list, board: board)
+ done = project.board.done_list
described_class.new(project, user).execute(development)
@@ -31,14 +31,14 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'does not remove list from board when list type is backlog' do
- list = create(:backlog_list, board: board)
+ list = project.board.backlog_list
service = described_class.new(project, user)
expect { service.execute(list) }.not_to change(board.lists, :count)
end
it 'does not remove list from board when list type is done' do
- list = create(:done_list, board: board)
+ list = project.board.done_list
service = described_class.new(project, user)
expect { service.execute(list) }.not_to change(board.lists, :count)
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
index 3e9b7d07fc6..102ed67449d 100644
--- a/spec/services/boards/lists/move_service_spec.rb
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -6,12 +6,12 @@ describe Boards::Lists::MoveService, services: true do
let(:board) { project.board }
let(:user) { create(:user) }
- let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:backlog) { project.board.backlog_list }
let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
let!(:staging) { create(:list, board: board, position: 3) }
- let!(:done) { create(:done_list, board: board) }
+ let!(:done) { project.board.done_list }
context 'when list type is set to label' do
it 'keeps position of lists when new position is nil' do
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 8da2a2b3c1b..343b4385bf2 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -41,7 +41,7 @@ describe CreateDeploymentService, services: true do
context 'for environment with invalid name' do
let(:params) do
- { environment: 'name with spaces',
+ { environment: 'name,with,commas',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
@@ -56,8 +56,36 @@ describe CreateDeploymentService, services: true do
expect(subject).not_to be_persisted
end
end
+
+ context 'when variables are used' do
+ let(:params) do
+ { environment: 'review-apps/$CI_BUILD_REF_NAME',
+ ref: 'master',
+ tag: false,
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ options: {
+ name: 'review-apps/$CI_BUILD_REF_NAME',
+ url: 'http://$CI_BUILD_REF_NAME.review-apps.gitlab.com'
+ },
+ variables: [
+ { key: 'CI_BUILD_REF_NAME', value: 'feature-review-apps' }
+ ]
+ }
+ end
+
+ it 'does create a new environment' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(subject.environment.name).to eq('review-apps/feature-review-apps')
+ expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com')
+ end
+
+ it 'does create a new deployment' do
+ expect(subject).to be_persisted
+ end
+ end
end
-
+
describe 'processing of builds' do
let(:environment) { nil }
@@ -95,6 +123,12 @@ describe CreateDeploymentService, services: true do
expect(Deployment.last.deployable).to eq(deployable)
end
+
+ it 'create environment has URL set' do
+ subject
+
+ expect(Deployment.last.environment.external_url).not_to be_nil
+ end
end
context 'without environment specified' do
@@ -107,7 +141,10 @@ describe CreateDeploymentService, services: true do
context 'when environment is specified' do
let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production') }
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production', options: options) }
+ let(:options) do
+ { environment: { name: 'production', url: 'http://gitlab.com' } }
+ end
context 'when build succeeds' do
it_behaves_like 'does create environment and deployment' do
@@ -132,4 +169,83 @@ describe CreateDeploymentService, services: true do
end
end
end
+
+ describe "merge request metrics" do
+ let(:params) do
+ {
+ environment: 'production',
+ ref: 'master',
+ tag: false,
+ sha: '97de212e80737a608d939f648d959671fb0a0142b',
+ }
+ end
+
+ let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) }
+
+ context "while updating the 'first_deployed_to_production_at' time" do
+ before { merge_request.mark_as_merged }
+
+ context "for merge requests merged before the current deploy" do
+ it "sets the time if the deploy's environment is 'production'" do
+ time = Time.now
+ Timecop.freeze(time) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time)
+ end
+
+ it "doesn't set the time if the deploy's environment is not 'production'" do
+ staging_params = params.merge(environment: 'staging')
+ service = described_class.new(project, user, staging_params)
+ service.execute
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+ end
+
+ it 'does not raise errors if the merge request does not have a metrics record' do
+ merge_request.metrics.destroy
+
+ expect(merge_request.reload.metrics).to be_nil
+ expect { service.execute }.not_to raise_error
+ end
+ end
+
+ context "for merge requests merged before the previous deploy" do
+ context "if the 'first_deployed_to_production_at' time is already set" do
+ it "does not overwrite the older 'first_deployed_to_production_at' time" do
+ # Previous deploy
+ time = Time.now
+ Timecop.freeze(time) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time)
+
+ # Current deploy
+ service = described_class.new(project, user, params)
+ Timecop.freeze(time + 12.hours) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time)
+ end
+ end
+
+ context "if the 'first_deployed_to_production_at' time is not already set" do
+ it "does not overwrite the older 'first_deployed_to_production_at' time" do
+ # Previous deploy
+ time = 5.minutes.from_now
+ Timecop.freeze(time) { service.execute }
+
+ expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at
+
+ merge_request.reload.metrics.update(first_deployed_to_production_at: nil)
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+
+ # Current deploy
+ service = described_class.new(project, user, params)
+ Timecop.freeze(time + 12.hours) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 6ac1fa8f182..22991c5bc86 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -253,6 +253,21 @@ describe GitPushService, services: true do
expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end
+ it "when pushing a branch for the first time with an existing branch permission configured" do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+ create(:protected_branch, :no_one_can_push, :developers_can_merge, project: project, name: 'master')
+ expect(project).to receive(:execute_hooks)
+ expect(project.default_branch).to eq("master")
+ expect_any_instance_of(ProtectedBranches::CreateService).not_to receive(:execute)
+
+ execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
+
+ expect(project.protected_branches).not_to be_empty
+ expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::NO_ACCESS])
+ expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
+ end
+
it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
@@ -324,6 +339,43 @@ describe GitPushService, services: true do
end
end
+ describe "issue metrics" do
+ let(:issue) { create :issue, project: project }
+ let(:commit_author) { create :user }
+ let(:commit) { project.commit }
+ let(:commit_time) { Time.now }
+
+ before do
+ project.team << [commit_author, :developer]
+ project.team << [user, :developer]
+
+ allow(commit).to receive_messages(
+ safe_message: "this commit \n mentions #{issue.to_reference}",
+ references: [issue],
+ author_name: commit_author.name,
+ author_email: commit_author.email,
+ committed_date: commit_time
+ )
+
+ allow(project.repository).to receive(:commits_between).and_return([commit])
+ end
+
+ context "while saving the 'first_mentioned_in_commit_at' metric for an issue" do
+ it 'sets the metric for referenced issues' do
+ execute_service(project, user, @oldrev, @newrev, @ref)
+
+ expect(issue.reload.metrics.first_mentioned_in_commit_at).to be_within(1.second).of(commit_time)
+ end
+
+ it 'does not set the metric for non-referenced issues' do
+ non_referenced_issue = create(:issue, project: project)
+ execute_service(project, user, @oldrev, @newrev, @ref)
+
+ expect(non_referenced_issue.reload.metrics.first_mentioned_in_commit_at).to be_nil
+ end
+ end
+ end
+
describe "closing issues from pushed commits containing a closing reference" do
let(:issue) { create :issue, project: project }
let(:other_issue) { create :issue, project: project }
diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index ac08aa53b0b..6f7ce8ca992 100644
--- a/spec/services/issues/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -1,14 +1,14 @@
require 'spec_helper'
-describe Issues::BulkUpdateService, services: true do
+describe Issuable::BulkUpdateService, services: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project, namespace: user.namespace) }
def bulk_update(issues, extra_params = {})
bulk_update_params = extra_params
- .reverse_merge(issues_ids: Array(issues).map(&:id).join(','))
+ .reverse_merge(issuable_ids: Array(issues).map(&:id).join(','))
- Issues::BulkUpdateService.new(project, user, bulk_update_params).execute
+ Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute('issue')
end
describe 'close issues' do
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index aff022a573e..5dfb33f4b28 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -18,12 +18,12 @@ describe Issues::CloseService, services: true do
context "valid params" do
before do
perform_enqueued_jobs do
- @issue = described_class.new(project, user, {}).execute(issue)
+ described_class.new(project, user).execute(issue)
end
end
- it { expect(@issue).to be_valid }
- it { expect(@issue).to be_closed }
+ it { expect(issue).to be_valid }
+ it { expect(issue).to be_closed }
it 'sends email to user2 about assign of new issue' do
email = ActionMailer::Base.deliveries.last
@@ -32,7 +32,7 @@ describe Issues::CloseService, services: true do
end
it 'creates system note about issue reassign' do
- note = @issue.notes.last
+ note = issue.notes.last
expect(note.note).to include "Status changed to closed"
end
@@ -44,23 +44,43 @@ describe Issues::CloseService, services: true do
context 'current user is not authorized to close issue' do
before do
perform_enqueued_jobs do
- @issue = described_class.new(project, guest).execute(issue)
+ described_class.new(project, guest).execute(issue)
end
end
it 'does not close the issue' do
- expect(@issue).to be_open
+ expect(issue).to be_open
end
end
- context "external issue tracker" do
+ context 'when issue is not confidential' do
+ it 'executes issue hooks' do
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
+
+ described_class.new(project, user).execute(issue)
+ end
+ end
+
+ context 'when issue is confidential' do
+ it 'executes confidential issue hooks' do
+ issue = create(:issue, :confidential, project: project)
+
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
+
+ described_class.new(project, user).execute(issue)
+ end
+ end
+
+ context 'external issue tracker' do
before do
allow(project).to receive(:default_issues_tracker?).and_return(false)
- @issue = described_class.new(project, user, {}).execute(issue)
+ described_class.new(project, user).execute(issue)
end
- it { expect(@issue).to be_valid }
- it { expect(@issue).to be_opened }
+ it { expect(issue).to be_valid }
+ it { expect(issue).to be_opened }
it { expect(todo.reload).to be_pending }
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index fcc3c0a00bd..1050502fa19 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -20,16 +20,38 @@ describe Issues::CreateService, services: true do
let(:opts) do
{ title: 'Awesome issue',
description: 'please fix',
- assignee: assignee,
+ assignee_id: assignee.id,
label_ids: labels.map(&:id),
- milestone_id: milestone.id }
+ milestone_id: milestone.id,
+ due_date: Date.tomorrow }
end
- it { expect(issue).to be_valid }
- it { expect(issue.title).to eq('Awesome issue') }
- it { expect(issue.assignee).to eq assignee }
- it { expect(issue.labels).to match_array labels }
- it { expect(issue.milestone).to eq milestone }
+ it 'creates the issue with the given params' do
+ expect(issue).to be_persisted
+ expect(issue.title).to eq('Awesome issue')
+ expect(issue.assignee).to eq assignee
+ expect(issue.labels).to match_array labels
+ expect(issue.milestone).to eq milestone
+ expect(issue.due_date).to eq Date.tomorrow
+ end
+
+ context 'when current user cannot admin issues in the project' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ end
+
+ it 'filters out params that cannot be set without the :admin_issue permission' do
+ issue = described_class.new(project, guest, opts).execute
+
+ expect(issue).to be_persisted
+ expect(issue.title).to eq('Awesome issue')
+ expect(issue.assignee).to be_nil
+ expect(issue.labels).to be_empty
+ expect(issue.milestone).to be_nil
+ expect(issue.due_date).to be_nil
+ end
+ end
it 'creates a pending todo for new assignee' do
attributes = {
@@ -72,6 +94,24 @@ describe Issues::CreateService, services: true do
expect(issue.milestone).not_to eq milestone
end
end
+
+ it 'executes issue hooks when issue is not confidential' do
+ opts = { title: 'Title', description: 'Description', confidential: false }
+
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
+
+ described_class.new(project, user, opts).execute
+ end
+
+ it 'executes confidential issue hooks when issue is confidential' do
+ opts = { title: 'Title', description: 'Description', confidential: true }
+
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
+
+ described_class.new(project, user, opts).execute
+ end
end
it_behaves_like 'new issuable record that supports slash commands'
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 34a89fcd4e1..93a8270fd16 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -1,24 +1,50 @@
require 'spec_helper'
describe Issues::ReopenService, services: true do
- let(:guest) { create(:user) }
- let(:issue) { create(:issue, :closed) }
- let(:project) { issue.project }
-
- before do
- project.team << [guest, :guest]
- end
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, :closed, project: project) }
describe '#execute' do
- context 'current user is not authorized to reopen issue' do
+ context 'when user is not authorized to reopen issue' do
before do
+ guest = create(:user)
+ project.team << [guest, :guest]
+
perform_enqueued_jobs do
- @issue = described_class.new(project, guest).execute(issue)
+ described_class.new(project, guest).execute(issue)
end
end
it 'does not reopen the issue' do
- expect(@issue).to be_closed
+ expect(issue).to be_closed
+ end
+ end
+
+ context 'when user is authrized to reopen issue' do
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ context 'when issue is not confidential' do
+ it 'executes issue hooks' do
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
+
+ described_class.new(project, user).execute(issue)
+ end
+ end
+
+ context 'when issue is confidential' do
+ it 'executes confidential issue hooks' do
+ issue = create(:issue, :confidential, :closed, project: project)
+
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
+
+ described_class.new(project, user).execute(issue)
+ end
end
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 0313f424463..1638a46ed51 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -23,76 +23,123 @@ describe Issues::UpdateService, services: true do
describe 'execute' do
def find_note(starting_with)
- @issue.notes.find do |note|
+ issue.notes.find do |note|
note && note.note.start_with?(starting_with)
end
end
- context "valid params" do
- before do
- opts = {
+ def update_issue(opts)
+ described_class.new(project, user, opts).execute(issue)
+ end
+
+ context 'valid params' do
+ let(:opts) do
+ {
title: 'New title',
description: 'Also please fix',
assignee_id: user2.id,
state_event: 'close',
label_ids: [label.id],
- confidential: true
+ due_date: Date.tomorrow
}
+ end
- perform_enqueued_jobs do
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
- end
+ it 'updates the issue with the given params' do
+ update_issue(opts)
- @issue.reload
+ expect(issue).to be_valid
+ expect(issue.title).to eq 'New title'
+ expect(issue.description).to eq 'Also please fix'
+ expect(issue.assignee).to eq user2
+ expect(issue).to be_closed
+ expect(issue.labels).to match_array [label]
+ expect(issue.due_date).to eq Date.tomorrow
end
- it { expect(@issue).to be_valid }
- it { expect(@issue.title).to eq('New title') }
- it { expect(@issue.assignee).to eq(user2) }
- it { expect(@issue).to be_closed }
- it { expect(@issue.labels.count).to eq(1) }
- it { expect(@issue.labels.first.title).to eq(label.name) }
-
- it 'sends 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
+ context 'when current user cannot admin issues in the project' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ end
- it 'creates system note about issue reassign' do
- note = find_note('Reassigned to')
+ it 'filters out params that cannot be set without the :admin_issue permission' do
+ described_class.new(project, guest, opts).execute(issue)
- expect(note).not_to be_nil
- expect(note.note).to include "Reassigned to \@#{user2.username}"
+ expect(issue).to be_valid
+ expect(issue.title).to eq 'New title'
+ expect(issue.description).to eq 'Also please fix'
+ expect(issue.assignee).to eq user3
+ expect(issue.labels).to be_empty
+ expect(issue.milestone).to be_nil
+ expect(issue.due_date).to be_nil
+ end
end
- it 'creates system note about issue label edit' do
- note = find_note('Added ~')
+ context 'with background jobs processed' do
+ before do
+ perform_enqueued_jobs do
+ update_issue(opts)
+ end
+ end
- expect(note).not_to be_nil
- expect(note.note).to include "Added ~#{label.id} label"
- end
+ it 'sends 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
- it 'creates system note about title change' do
- note = find_note('Changed title:')
+ it 'creates system note about issue reassign' do
+ note = find_note('Reassigned to')
- expect(note).not_to be_nil
- expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
+ expect(note).not_to be_nil
+ expect(note.note).to include "Reassigned to \@#{user2.username}"
+ end
+
+ it 'creates system note about issue label edit' do
+ note = find_note('Added ~')
+
+ expect(note).not_to be_nil
+ expect(note.note).to include "Added ~#{label.id} label"
+ end
+
+ it 'creates system note about title change' do
+ note = find_note('Changed title:')
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
+ end
+ end
+ end
+
+ context 'when issue turns confidential' do
+ let(:opts) do
+ {
+ title: 'New title',
+ description: 'Also please fix',
+ assignee_id: user2.id,
+ state_event: 'close',
+ label_ids: [label.id],
+ confidential: true
+ }
end
it 'creates system note about confidentiality change' do
+ update_issue(confidential: true)
+
note = find_note('Made the issue confidential')
expect(note).not_to be_nil
expect(note.note).to eq 'Made the issue confidential'
end
- end
- def update_issue(opts)
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
- @issue.reload
+ it 'executes confidential issue hooks' do
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
+
+ update_issue(confidential: true)
+ end
end
context 'todos' do
@@ -100,7 +147,7 @@ describe Issues::UpdateService, services: true do
context 'when the title change' do
before do
- update_issue({ title: 'New title' })
+ update_issue(title: 'New title')
end
it 'marks pending todos as done' do
@@ -110,7 +157,7 @@ describe Issues::UpdateService, services: true do
context 'when the description change' do
before do
- update_issue({ description: 'Also please fix' })
+ update_issue(description: 'Also please fix')
end
it 'marks todos as done' do
@@ -120,7 +167,7 @@ describe Issues::UpdateService, services: true do
context 'when is reassigned' do
before do
- update_issue({ assignee: user2 })
+ update_issue(assignee: user2)
end
it 'marks previous assignee todos as done' do
@@ -144,7 +191,7 @@ describe Issues::UpdateService, services: true do
context 'when the milestone change' do
before do
- update_issue({ milestone: create(:milestone) })
+ update_issue(milestone: create(:milestone))
end
it 'marks todos as done' do
@@ -154,7 +201,7 @@ describe Issues::UpdateService, services: true do
context 'when the labels change' do
before do
- update_issue({ label_ids: [label.id] })
+ update_issue(label_ids: [label.id])
end
it 'marks todos as done' do
@@ -165,6 +212,7 @@ describe Issues::UpdateService, services: true do
context 'when the issue is relabeled' do
let!(:non_subscriber) { create(:user) }
+
let!(:subscriber) do
create(:user).tap do |u|
label.toggle_subscription(u)
@@ -176,7 +224,7 @@ describe Issues::UpdateService, services: true do
opts = { label_ids: [label.id] }
perform_enqueued_jobs do
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ @issue = described_class.new(project, user, opts).execute(issue)
end
should_email(subscriber)
@@ -190,7 +238,7 @@ describe Issues::UpdateService, services: true do
opts = { label_ids: [label.id, label2.id] }
perform_enqueued_jobs do
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ @issue = described_class.new(project, user, opts).execute(issue)
end
should_not_email(subscriber)
@@ -201,7 +249,7 @@ describe Issues::UpdateService, services: true do
opts = { label_ids: [label2.id] }
perform_enqueued_jobs do
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ @issue = described_class.new(project, user, opts).execute(issue)
end
should_not_email(subscriber)
@@ -210,13 +258,15 @@ describe Issues::UpdateService, services: true do
end
end
- context 'when Issue has tasks' do
- before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
+ context 'when issue has tasks' do
+ before do
+ update_issue(description: "- [ ] Task 1\n- [ ] Task 2")
+ end
- it { expect(@issue.tasks?).to eq(true) }
+ 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" }) }
+ 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')
@@ -229,8 +279,8 @@ describe Issues::UpdateService, services: true do
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" })
+ 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
@@ -244,8 +294,8 @@ describe Issues::UpdateService, services: true do
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" })
+ 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
@@ -257,8 +307,8 @@ describe Issues::UpdateService, services: true do
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" })
+ 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
@@ -269,7 +319,7 @@ describe Issues::UpdateService, services: true do
it 'does not generate a new note at all' do
expect do
- update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
+ update_issue(description: "- [ ] One\n- [ ] Two\n- [ ] Three")
end.not_to change { Note.count }
end
end
@@ -277,7 +327,7 @@ describe Issues::UpdateService, services: true do
context 'updating labels' do
let(:label3) { create(:label, project: project) }
- let(:result) { Issues::UpdateService.new(project, user, params).execute(issue).reload }
+ let(:result) { described_class.new(project, user, params).execute(issue).reload }
context 'when add_label_ids and label_ids are passed' do
let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } }
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
new file mode 100644
index 00000000000..6fca80b5613
--- /dev/null
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+
+describe Members::ApproveAccessRequestService, services: true do
+ let(:user) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:group) { create(:group, :public) }
+
+ shared_examples 'a service raising ActiveRecord::RecordNotFound' do
+ it 'raises ActiveRecord::RecordNotFound' do
+ expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
+ it 'raises Gitlab::Access::AccessDeniedError' do
+ expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ shared_examples 'a service approving an access request' do
+ it 'succeeds' do
+ expect { described_class.new(source, user, params).execute }.to change { source.requesters.count }.by(-1)
+ end
+
+ it 'returns a <Source>Member' do
+ member = described_class.new(source, user, params).execute
+
+ expect(member).to be_a "#{source.class.to_s}Member".constantize
+ expect(member.requested_at).to be_nil
+ end
+
+ context 'with a custom access level' do
+ let(:params) { { user_id: access_requester.id, access_level: Gitlab::Access::MASTER } }
+
+ it 'returns a ProjectMember with the custom access level' do
+ member = described_class.new(source, user, params).execute
+
+ expect(member.access_level).to eq Gitlab::Access::MASTER
+ end
+ end
+ end
+
+ context 'when no access requester are found' do
+ let(:params) { { user_id: 42 } }
+
+ it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
+ let(:source) { group }
+ end
+ end
+
+ context 'when an access requester is found' do
+ before do
+ project.request_access(access_requester)
+ group.request_access(access_requester)
+ end
+ let(:params) { { user_id: access_requester.id } }
+
+ context 'when current user cannot approve access request to the project' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { group }
+ end
+ end
+
+ context 'when current user can approve access request to the project' do
+ before do
+ project.team << [user, :master]
+ group.add_owner(user)
+ end
+
+ it_behaves_like 'a service approving an access request' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service approving an access request' do
+ let(:source) { group }
+ end
+
+ context 'when given a :id' do
+ let(:params) { { id: project.requesters.find_by!(user_id: access_requester.id).id } }
+
+ it_behaves_like 'a service approving an access request' do
+ let(:source) { project }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb
new file mode 100644
index 00000000000..dff5b4917ae
--- /dev/null
+++ b/spec/services/members/request_access_service_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Members::RequestAccessService, services: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :private) }
+ let(:group) { create(:group, :private) }
+
+ shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
+ it 'raises Gitlab::Access::AccessDeniedError' do
+ expect { described_class.new(source, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ shared_examples 'a service creating a access request' do
+ it 'succeeds' do
+ expect { described_class.new(source, user).execute }.to change { source.requesters.count }.by(1)
+ end
+
+ it 'returns a <Source>Member' do
+ member = described_class.new(source, user).execute
+
+ expect(member).to be_a "#{source.class.to_s}Member".constantize
+ expect(member.requested_at).to be_present
+ end
+ end
+
+ context 'when source is nil' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { nil }
+ end
+ end
+
+ context 'when current user cannot request access to the project' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { group }
+ end
+ end
+
+ context 'when current user can request access to the project' do
+ before do
+ project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ group.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it_behaves_like 'a service creating a access request' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service creating a access request' do
+ let(:source) { group }
+ end
+ end
+end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 232508cda23..0d586e2216b 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -99,14 +99,14 @@ describe MergeRequests::BuildService, services: true do
let(:source_branch) { "#{issue.iid}-fix-issue" }
it 'appends "Closes #$issue-iid" to the description' do
- expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\nCloses ##{issue.iid}")
+ expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\n\nCloses ##{issue.iid}")
end
context 'merge request already has a description set' do
let(:description) { 'Merge request description' }
it 'appends "Closes #$issue-iid" to the description' do
- expect(merge_request.description).to eq("#{description}\nCloses ##{issue.iid}")
+ expect(merge_request.description).to eq("#{description}\n\nCloses ##{issue.iid}")
end
end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index c1e4f8bd96b..b8142889075 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -83,5 +83,34 @@ describe MergeRequests::CreateService, services: true do
}
end
end
+
+ context 'while saving references to issues that the created merge request closes' do
+ let(:first_issue) { create(:issue, project: project) }
+ let(:second_issue) { create(:issue, project: project) }
+
+ let(:opts) do
+ {
+ title: 'Awesome merge_request',
+ source_branch: 'feature',
+ target_branch: 'master',
+ force_remove_source_branch: '1'
+ }
+ end
+
+ before do
+ project.team << [user, :master]
+ project.team << [assignee, :developer]
+ end
+
+ it 'creates a `MergeRequestsClosingIssues` record for each issue' do
+ issue_closing_opts = opts.merge(description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}")
+ service = described_class.new(project, user, issue_closing_opts)
+ allow(service).to receive(:execute_hooks)
+ merge_request = service.execute
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to match_array([first_issue.id, second_issue.id])
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 159f6817e8d..31167675d07 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -38,6 +38,30 @@ describe MergeRequests::MergeService, services: true do
end
end
+ context 'closes related todos' do
+ let(:merge_request) { create(:merge_request, assignee: user, author: user) }
+ let(:project) { merge_request.project }
+ let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
+ let!(:todo) do
+ create(:todo, :assigned,
+ project: project,
+ author: user,
+ user: user,
+ target: merge_request)
+ end
+
+ before do
+ allow(service).to receive(:execute_hooks)
+
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ todo.reload
+ end
+ end
+
+ it { expect(todo).to be_done }
+ end
+
context 'remove source branch by author' do
let(:service) do
merge_request.merge_params['force_remove_source_branch'] = '1'
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index fff86480c6d..59d3912018a 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -79,8 +79,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ it { expect(@build_failed_todo).to be_done }
+ it { expect(@fork_build_failed_todo).to be_done }
end
context 'manual merge of source branch' do
@@ -99,8 +99,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.diffs.size).to be > 0 }
it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ it { expect(@build_failed_todo).to be_done }
+ it { expect(@fork_build_failed_todo).to be_done }
end
context 'push to fork repo source branch' do
@@ -149,8 +149,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ it { expect(@build_failed_todo).to be_done }
+ it { expect(@fork_build_failed_todo).to be_done }
end
context 'push new branch that exists in a merge request' do
@@ -174,6 +174,58 @@ describe MergeRequests::RefreshService, services: true do
end
end
+ context 'merge request metrics' do
+ let(:issue) { create :issue, project: @project }
+ let(:commit_author) { create :user }
+ let(:commit) { project.commit }
+
+ before do
+ project.team << [commit_author, :developer]
+ project.team << [user, :developer]
+
+ allow(commit).to receive_messages(
+ safe_message: "Closes #{issue.to_reference}",
+ references: [issue],
+ author_name: commit_author.name,
+ author_email: commit_author.email,
+ committed_date: Time.now
+ )
+
+ allow_any_instance_of(MergeRequest).to receive(:commits).and_return([commit])
+ end
+
+ context 'when the merge request is sourced from the same project' do
+ it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
+ merge_request = create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: @project)
+ refresh_service = service.new(@project, @user)
+ allow(refresh_service).to receive(:execute_hooks)
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature')
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to eq([issue.id])
+ end
+ end
+
+ context 'when the merge request is sourced from a different project' do
+ it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
+ forked_project = create(:project)
+ create(:forked_project_link, forked_to_project: forked_project, forked_from_project: @project)
+
+ merge_request = create(:merge_request,
+ target_branch: 'master',
+ source_branch: 'feature',
+ target_project: @project,
+ source_project: forked_project)
+ refresh_service = service.new(@project, @user)
+ allow(refresh_service).to receive(:execute_hooks)
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature')
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to eq([issue.id])
+ end
+ end
+ end
+
def reload_mrs
@merge_request.reload
@fork_merge_request.reload
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 6dfeb581975..33db34c0f62 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -263,5 +263,42 @@ describe MergeRequests::UpdateService, services: true do
end
end
end
+
+ context 'while saving references to issues that the updated merge request closes' do
+ let(:first_issue) { create(:issue, project: project) }
+ let(:second_issue) { create(:issue, project: project) }
+
+ it 'creates a `MergeRequestsClosingIssues` record for each issue' do
+ issue_closing_opts = { description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}" }
+ service = described_class.new(project, user, issue_closing_opts)
+ allow(service).to receive(:execute_hooks)
+ service.execute(merge_request)
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to match_array([first_issue.id, second_issue.id])
+ end
+
+ it 'removes `MergeRequestsClosingIssues` records when issues are not closed anymore' do
+ opts = {
+ title: 'Awesome merge_request',
+ description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}",
+ source_branch: 'feature',
+ target_branch: 'master',
+ force_remove_source_branch: '1'
+ }
+
+ merge_request = MergeRequests::CreateService.new(project, user, opts).execute
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to match_array([first_issue.id, second_issue.id])
+
+ service = described_class.new(project, user, description: "not closing any issues")
+ allow(service).to receive(:execute_hooks)
+ service.execute(merge_request.reload)
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to be_empty
+ end
+ end
end
end
diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb
index 4f231aab161..d1099884a02 100644
--- a/spec/services/notes/slash_commands_service_spec.rb
+++ b/spec/services/notes/slash_commands_service_spec.rb
@@ -122,6 +122,75 @@ describe Notes::SlashCommandsService, services: true do
end
end
+ describe '.noteable_update_service' do
+ include_context 'note on noteable'
+
+ it 'returns Issues::UpdateService for a note on an issue' do
+ note = create(:note_on_issue, project: project)
+
+ expect(described_class.noteable_update_service(note)).to eq(Issues::UpdateService)
+ end
+
+ it 'returns Issues::UpdateService for a note on a merge request' do
+ note = create(:note_on_merge_request, project: project)
+
+ expect(described_class.noteable_update_service(note)).to eq(MergeRequests::UpdateService)
+ end
+
+ it 'returns nil for a note on a commit' do
+ note = create(:note_on_commit, project: project)
+
+ expect(described_class.noteable_update_service(note)).to be_nil
+ end
+ end
+
+ describe '.supported?' do
+ include_context 'note on noteable'
+
+ let(:note) { create(:note_on_issue, project: project) }
+
+ context 'with no current_user' do
+ it 'returns false' do
+ expect(described_class.supported?(note, nil)).to be_falsy
+ end
+ end
+
+ context 'when current_user cannot update the noteable' do
+ it 'returns false' do
+ user = create(:user)
+
+ expect(described_class.supported?(note, user)).to be_falsy
+ end
+ end
+
+ context 'when current_user can update the noteable' do
+ it 'returns true' do
+ expect(described_class.supported?(note, master)).to be_truthy
+ end
+
+ context 'with a note on a commit' do
+ let(:note) { create(:note_on_commit, project: project) }
+
+ it 'returns false' do
+ expect(described_class.supported?(note, nil)).to be_falsy
+ end
+ end
+ end
+ end
+
+ describe '#supported?' do
+ include_context 'note on noteable'
+
+ it 'delegates to the class method' do
+ service = described_class.new(project, master)
+ note = create(:note_on_issue, project: project)
+
+ expect(described_class).to receive(:supported?).with(note, master)
+
+ service.supported?(note)
+ end
+ end
+
describe '#execute' do
let(:service) { described_class.new(project, master) }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index f81a58899fd..d820646ebdf 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -379,6 +379,7 @@ describe NotificationService, services: true do
it "emails subscribers of the issue's labels" do
subscriber = create(:user)
label = create(:label, issues: [issue])
+ issue.reload
label.toggle_subscription(subscriber)
notification.new_issue(issue, @u_disabled)
@@ -399,6 +400,7 @@ describe NotificationService, services: true do
project.team << [guest, :guest]
label = create(:label, issues: [confidential_issue])
+ confidential_issue.reload
label.toggle_subscription(non_member)
label.toggle_subscription(author)
label.toggle_subscription(assignee)
@@ -960,6 +962,20 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant)
end
+ it "notifies the merger when merge_when_build_succeeds is true" do
+ merge_request.merge_when_build_succeeds = true
+ notification.merge_mr(merge_request, @u_watcher)
+
+ should_email(@u_watcher)
+ end
+
+ it "does not notify the merger when merge_when_build_succeeds is false" do
+ merge_request.merge_when_build_succeeds = false
+ notification.merge_mr(merge_request, @u_watcher)
+
+ should_not_email(@u_watcher)
+ end
+
context 'participating' do
context 'by assignee' do
before do
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index ad0d58672b3..cf90b33dfb4 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -4,12 +4,15 @@ describe Projects::HousekeepingService do
subject { Projects::HousekeepingService.new(project) }
let(:project) { create :project }
- describe 'execute' do
- before do
- project.pushes_since_gc = 3
- project.save!
- end
+ before do
+ project.reset_pushes_since_gc
+ end
+
+ after do
+ project.reset_pushes_since_gc
+ end
+ describe '#execute' do
it 'enqueues a sidekiq job' do
expect(subject).to receive(:try_obtain_lease).and_return(true)
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id)
@@ -32,12 +35,12 @@ describe Projects::HousekeepingService do
it 'does not reset pushes_since_gc' do
expect do
expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
- end.not_to change { project.pushes_since_gc }.from(3)
+ end.not_to change { project.pushes_since_gc }
end
end
end
- describe 'needed?' do
+ describe '#needed?' do
it 'when the count is low enough' do
expect(subject.needed?).to eq(false)
end
@@ -48,25 +51,11 @@ describe Projects::HousekeepingService do
end
end
- describe 'increment!' do
- let(:lease_key) { "project_housekeeping:increment!:#{project.id}" }
-
+ describe '#increment!' do
it 'increments the pushes_since_gc counter' do
- lease = double(:lease, try_obtain: true)
- expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease)
-
expect do
subject.increment!
end.to change { project.pushes_since_gc }.from(0).to(1)
end
-
- it 'does not increment when no lease can be obtained' do
- lease = double(:lease, try_obtain: false)
- expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease)
-
- expect do
- subject.increment!
- end.not_to change { project.pushes_since_gc }
- end
end
end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index d5d4d7c56ef..ed1384798ab 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -108,6 +108,16 @@ describe Projects::ImportService, services: true do
expect(result[:status]).to eq :error
expect(result[:message]).to eq 'Github: failed to connect API'
end
+
+ it 'expires existence cache after error' do
+ allow_any_instance_of(Project).to receive(:repository_exists?).and_return(true)
+
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
+ expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).and_call_original
+ expect_any_instance_of(Repository).to receive(:expire_exists_cache).and_call_original
+
+ subject.execute
+ end
end
def stub_github_omniauth_provider
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
new file mode 100644
index 00000000000..7d4eff3b6ef
--- /dev/null
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe ProtectedBranches::CreateService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { project.owner }
+ let(:params) do
+ {
+ name: 'master',
+ merge_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ],
+ push_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ]
+ }
+ end
+
+ describe '#execute' do
+ subject(:service) { described_class.new(project, user, params) }
+
+ it 'creates a new protected branch' do
+ expect { service.execute }.to change(ProtectedBranch, :count).by(1)
+ expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ end
+ end
+end
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index a616275e883..5b1edba87a1 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -1,19 +1,19 @@
require 'spec_helper'
describe SlashCommands::InterpretService, services: true do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+ let(:developer) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:milestone) { create(:milestone, project: project, title: '9.10') }
let(:inprogress) { create(:label, project: project, title: 'In Progress') }
let(:bug) { create(:label, project: project, title: 'Bug') }
before do
- project.team << [user, :developer]
+ project.team << [developer, :developer]
end
describe '#execute' do
- let(:service) { described_class.new(project, user) }
+ let(:service) { described_class.new(project, developer) }
let(:merge_request) { create(:merge_request, source_project: project) }
shared_examples 'reopen command' do
@@ -45,13 +45,13 @@ describe SlashCommands::InterpretService, services: true do
it 'fetches assignee and populates assignee_id if content contains /assign' do
_, updates = service.execute(content, issuable)
- expect(updates).to eq(assignee_id: user.id)
+ expect(updates).to eq(assignee_id: developer.id)
end
end
shared_examples 'unassign command' do
it 'populates assignee_id: nil if content contains /unassign' do
- issuable.update(assignee_id: user.id)
+ issuable.update(assignee_id: developer.id)
_, updates = service.execute(content, issuable)
expect(updates).to eq(assignee_id: nil)
@@ -124,7 +124,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'done command' do
it 'populates todo_event: "done" if content contains /done' do
- TodoService.new.mark_todo(issuable, user)
+ TodoService.new.mark_todo(issuable, developer)
_, updates = service.execute(content, issuable)
expect(updates).to eq(todo_event: 'done')
@@ -141,7 +141,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unsubscribe command' do
it 'populates subscription_event: "unsubscribe" if content contains /unsubscribe' do
- issuable.subscribe(user)
+ issuable.subscribe(developer)
_, updates = service.execute(content, issuable)
expect(updates).to eq(subscription_event: 'unsubscribe')
@@ -209,12 +209,12 @@ describe SlashCommands::InterpretService, services: true do
end
it_behaves_like 'assign command' do
- let(:content) { "/assign @#{user.username}" }
+ let(:content) { "/assign @#{developer.username}" }
let(:issuable) { issue }
end
it_behaves_like 'assign command' do
- let(:content) { "/assign @#{user.username}" }
+ let(:content) { "/assign @#{developer.username}" }
let(:issuable) { merge_request }
end
@@ -380,5 +380,56 @@ describe SlashCommands::InterpretService, services: true do
let(:content) { '/remove_due_date' }
let(:issuable) { merge_request }
end
+
+ context 'when current_user cannot :admin_issue' do
+ let(:visitor) { create(:user) }
+ let(:issue) { create(:issue, project: project, author: visitor) }
+ let(:service) { described_class.new(project, visitor) }
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/assign @#{developer.username}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/unassign' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/milestone %#{milestone.title}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/remove_milestone' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { %(/unlabel ~"#{inprogress.title}") }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { %(/relabel ~"#{inprogress.title}") }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/due tomorrow' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/remove_due_date' }
+ let(:issuable) { issue }
+ end
+ end
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 3d854a959f3..b16840a1238 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -40,6 +40,12 @@ describe SystemNoteService, services: true do
describe 'note body' do
let(:note_lines) { subject.note.split("\n").reject(&:blank?) }
+ describe 'comparison diff link line' do
+ it 'adds the comparison text' do
+ expect(note_lines[2]).to match "[Compare with previous version]"
+ end
+ end
+
context 'without existing commits' do
it 'adds a message header' do
expect(note_lines[0]).to eq "Added #{new_commits.size} commits:"
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index cafcad3e3c0..b41f6f14fbd 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -145,6 +145,14 @@ describe TodoService, services: true do
end
end
+ describe '#destroy_issue' do
+ it 'refresh the todos count cache for the user' do
+ expect(john_doe).to receive(:update_todos_count_cache).and_call_original
+
+ service.destroy_issue(issue, john_doe)
+ end
+ end
+
describe '#reassigned_issue' do
it 'creates a pending todo for new assignee' do
unassigned_issue.update_attribute(:assignee, john_doe)
@@ -394,6 +402,14 @@ describe TodoService, services: true do
end
end
+ describe '#destroy_merge_request' do
+ it 'refresh the todos count cache for the user' do
+ expect(john_doe).to receive(:update_todos_count_cache).and_call_original
+
+ service.destroy_merge_request(mr_assigned, john_doe)
+ end
+ end
+
describe '#reassigned_merge_request' do
it 'creates a pending todo for new assignee' do
mr_unassigned.update_attribute(:assignee, john_doe)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 02b2b3ca101..b19f5824236 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -26,7 +26,7 @@ RSpec.configure do |config|
config.verbose_retry = true
config.display_try_failure_messages = true
- config.include Devise::TestHelpers, type: :controller
+ config.include Devise::Test::ControllerHelpers, type: :controller
config.include Warden::Test::Helpers, type: :request
config.include LoginHelpers, type: :feature
config.include StubConfiguration
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
new file mode 100644
index 00000000000..62a5b46d47b
--- /dev/null
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -0,0 +1,68 @@
+module CycleAnalyticsHelpers
+ def create_commit_referencing_issue(issue, branch_name: random_git_name)
+ project.repository.add_branch(user, branch_name, 'master')
+ create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
+ end
+
+ def create_commit(message, project, user, branch_name, count: 1)
+ oldrev = project.repository.commit(branch_name).sha
+ commit_shas = Array.new(count) do |index|
+ filename = random_git_name
+
+ options = {
+ committer: project.repository.user_to_committer(user),
+ author: project.repository.user_to_committer(user),
+ commit: { message: message, branch: branch_name, update_ref: true },
+ file: { content: "content", path: filename, update: false }
+ }
+
+ commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
+ project.repository.commit(commit_sha)
+
+ commit_sha
+ end
+
+ GitPushService.new(project,
+ user,
+ oldrev: oldrev,
+ newrev: commit_shas.last,
+ ref: 'refs/heads/master').execute
+ end
+
+ def create_merge_request_closing_issue(issue, message: nil, source_branch: nil)
+ if !source_branch || project.repository.commit(source_branch).blank?
+ source_branch = random_git_name
+ project.repository.add_branch(user, source_branch, 'master')
+ end
+
+ sha = project.repository.commit_file(user, random_git_name, "content", "commit message", source_branch, false)
+ project.repository.commit(sha)
+
+ opts = {
+ title: 'Awesome merge_request',
+ description: message || "Fixes #{issue.to_reference}",
+ source_branch: source_branch,
+ target_branch: 'master'
+ }
+
+ MergeRequests::CreateService.new(project, user, opts).execute
+ end
+
+ def merge_merge_requests_closing_issue(issue)
+ merge_requests = issue.closed_by_merge_requests
+ merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
+ end
+
+ def deploy_master(environment: 'production')
+ CreateDeploymentService.new(project, user, {
+ environment: environment,
+ ref: 'master',
+ tag: false,
+ sha: project.repository.commit('master').sha
+ }).execute
+ end
+end
+
+RSpec.configure do |config|
+ config.include CycleAnalyticsHelpers
+end
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
new file mode 100644
index 00000000000..8e19a6c92e2
--- /dev/null
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -0,0 +1,161 @@
+# rubocop:disable Metrics/AbcSize
+
+# Note: The ABC size is large here because we have a method generating test cases with
+# multiple nested contexts. This shouldn't count as a violation.
+
+module CycleAnalyticsHelpers
+ module TestGeneration
+ # Generate the most common set of specs that all cycle analytics phases need to have.
+ #
+ # Arguments:
+ #
+ # phase: Which phase are we testing? Will call `CycleAnalytics.new.send(phase)` for the final assertion
+ # data_fn: A function that returns a hash, constituting initial data for the test case
+ # start_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
+ # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
+ # Each `condition_fn` is expected to implement a case which consitutes the start of the given cycle analytics phase.
+ # end_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
+ # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
+ # Each `condition_fn` is expected to implement a case which consitutes the end of the given cycle analytics phase.
+ # before_end_fn: This function is run before calling the end time conditions. Used for setup that needs to be run between the start and end conditions.
+ # post_fn: Code that needs to be run after running the end time conditions.
+
+ def generate_cycle_analytics_spec(phase:, data_fn:, start_time_conditions:, end_time_conditions:, before_end_fn: nil, post_fn: nil)
+ combinations_of_start_time_conditions = (1..start_time_conditions.size).flat_map { |size| start_time_conditions.combination(size).to_a }
+ combinations_of_end_time_conditions = (1..end_time_conditions.size).flat_map { |size| end_time_conditions.combination(size).to_a }
+
+ scenarios = combinations_of_start_time_conditions.product(combinations_of_end_time_conditions)
+ scenarios.each do |start_time_conditions, end_time_conditions|
+ context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
+ context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
+ it "finds the median of available durations between the two conditions" do
+ time_differences = Array.new(5) do |index|
+ data = data_fn[self]
+ start_time = (index * 10).days.from_now
+ end_time = start_time + rand(1..5).days
+
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
+
+ # Run `before_end_fn` at the midpoint between `start_time` and `end_time`
+ Timecop.freeze(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn
+
+ end_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(end_time) { condition_fn[self, data] }
+ end
+
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
+ end_time - start_time
+ end
+
+ median_time_difference = time_differences.sort[2]
+ expect(subject.send(phase)).to be_within(5).of(median_time_difference)
+ end
+
+ context "when the data belongs to another project" do
+ let(:other_project) { create(:project) }
+
+ it "returns nil" do
+ # Use a stub to "trick" the data/condition functions
+ # into using another project. This saves us from having to
+ # define separate data/condition functions for this particular
+ # test case.
+ allow(self).to receive(:project) { other_project }
+
+ 5.times do
+ data = data_fn[self]
+ start_time = Time.now
+ end_time = rand(1..10).days.from_now
+
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
+
+ end_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(end_time) { condition_fn[self, data] }
+ end
+
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ end
+
+ # Turn off the stub before checking assertions
+ allow(self).to receive(:project).and_call_original
+
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+
+ context "when the end condition happens before the start condition" do
+ it 'returns nil' do
+ data = data_fn[self]
+ start_time = Time.now
+ end_time = start_time + rand(1..5).days
+
+ # Run `before_end_fn` at the midpoint between `start_time` and `end_time`
+ Timecop.freeze(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn
+
+ end_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
+
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(end_time) { condition_fn[self, data] }
+ end
+
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+ end
+ end
+
+ context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do
+ context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
+ it "returns nil" do
+ 5.times do
+ data = data_fn[self]
+ end_time = rand(1..10).days.from_now
+
+ end_time_conditions.each_with_index do |(condition_name, condition_fn), index|
+ Timecop.freeze(end_time + index.days) { condition_fn[self, data] }
+ end
+
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ end
+
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+ end
+
+ context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
+ context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do
+ it "returns nil" do
+ 5.times do
+ data = data_fn[self]
+ start_time = Time.now
+
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
+
+ post_fn[self, data] if post_fn
+ end
+
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+ end
+ end
+
+ context "when none of the start / end conditions are matched" do
+ it "returns nil" do
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index e0dbc9aa84c..ac38e31b77e 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -15,7 +15,7 @@ RSpec.configure do |config|
DatabaseCleaner.start
end
- config.after(:each) do
+ config.append_after(:each) do
DatabaseCleaner.clean
end
end
diff --git a/spec/support/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index d2a49ea5c5e..5e3b8f2b23e 100644
--- a/spec/support/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -2,6 +2,9 @@
# It takes a `issuable_type`, and expect an `issuable`.
shared_examples 'issuable record that supports slash commands in its description and notes' do |issuable_type|
+ include SlashCommandsHelpers
+ include WaitForAjax
+
let(:master) { create(:user) }
let(:assignee) { create(:user, username: 'bob') }
let(:guest) { create(:user) }
@@ -18,6 +21,11 @@ shared_examples 'issuable record that supports slash commands in its description
login_with(master)
end
+ after do
+ # Ensure all outstanding Ajax requests are complete to avoid database deadlocks
+ wait_for_ajax
+ end
+
describe "new #{issuable_type}" do
context 'with commands in the description' do
it "creates the #{issuable_type} and interpret commands accordingly" do
@@ -44,10 +52,7 @@ shared_examples 'issuable record that supports slash commands in its description
context 'with a note containing commands' do
it 'creates a note without the commands and interpret the commands accordingly' do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\""
- click_button 'Comment'
- end
+ write_note("Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
expect(page).to have_content 'Awesome!'
expect(page).not_to have_content '/assign @bob'
@@ -66,10 +71,7 @@ shared_examples 'issuable record that supports slash commands in its description
context 'with a note containing only commands' do
it 'does not create a note but interpret the commands accordingly' do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/assign @bob\n/label ~bug\n/milestone %\"ASAP\""
- click_button 'Comment'
- end
+ write_note("/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
expect(page).not_to have_content '/assign @bob'
expect(page).not_to have_content '/label ~bug'
@@ -92,10 +94,7 @@ shared_examples 'issuable record that supports slash commands in its description
context "when current user can close #{issuable_type}" do
it "closes the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/close"
- click_button 'Comment'
- end
+ write_note("/close")
expect(page).not_to have_content '/close'
expect(page).to have_content 'Your commands have been executed!'
@@ -112,10 +111,7 @@ shared_examples 'issuable record that supports slash commands in its description
end
it "does not close the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/close"
- click_button 'Comment'
- end
+ write_note("/close")
expect(page).not_to have_content '/close'
expect(page).not_to have_content 'Your commands have been executed!'
@@ -133,10 +129,7 @@ shared_examples 'issuable record that supports slash commands in its description
context "when current user can reopen #{issuable_type}" do
it "reopens the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/reopen"
- click_button 'Comment'
- end
+ write_note("/reopen")
expect(page).not_to have_content '/reopen'
expect(page).to have_content 'Your commands have been executed!'
@@ -153,10 +146,7 @@ shared_examples 'issuable record that supports slash commands in its description
end
it "does not reopen the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/reopen"
- click_button 'Comment'
- end
+ write_note("/reopen")
expect(page).not_to have_content '/reopen'
expect(page).not_to have_content 'Your commands have been executed!'
@@ -169,10 +159,7 @@ shared_examples 'issuable record that supports slash commands in its description
context "with a note changing the #{issuable_type}'s title" do
context "when current user can change title of #{issuable_type}" do
it "reopens the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/title Awesome new title"
- click_button 'Comment'
- end
+ write_note("/title Awesome new title")
expect(page).not_to have_content '/title'
expect(page).to have_content 'Your commands have been executed!'
@@ -189,10 +176,7 @@ shared_examples 'issuable record that supports slash commands in its description
end
it "does not reopen the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/title Awesome new title"
- click_button 'Comment'
- end
+ write_note("/title Awesome new title")
expect(page).not_to have_content '/title'
expect(page).not_to have_content 'Your commands have been executed!'
@@ -204,10 +188,7 @@ shared_examples 'issuable record that supports slash commands in its description
context "with a note marking the #{issuable_type} as todo" do
it "creates a new todo for the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/todo"
- click_button 'Comment'
- end
+ write_note("/todo")
expect(page).not_to have_content '/todo'
expect(page).to have_content 'Your commands have been executed!'
@@ -238,10 +219,7 @@ shared_examples 'issuable record that supports slash commands in its description
expect(todo.author).to eq master
expect(todo.user).to eq master
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/done"
- click_button 'Comment'
- end
+ write_note("/done")
expect(page).not_to have_content '/done'
expect(page).to have_content 'Your commands have been executed!'
@@ -254,10 +232,7 @@ shared_examples 'issuable record that supports slash commands in its description
it "creates a new todo for the #{issuable_type}" do
expect(issuable.subscribed?(master)).to be_falsy
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/subscribe"
- click_button 'Comment'
- end
+ write_note("/subscribe")
expect(page).not_to have_content '/subscribe'
expect(page).to have_content 'Your commands have been executed!'
@@ -274,10 +249,7 @@ shared_examples 'issuable record that supports slash commands in its description
it "creates a new todo for the #{issuable_type}" do
expect(issuable.subscribed?(master)).to be_truthy
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/unsubscribe"
- click_button 'Comment'
- end
+ write_note("/unsubscribe")
expect(page).not_to have_content '/unsubscribe'
expect(page).to have_content 'Your commands have been executed!'
diff --git a/spec/support/git_helpers.rb b/spec/support/git_helpers.rb
new file mode 100644
index 00000000000..93422390ef7
--- /dev/null
+++ b/spec/support/git_helpers.rb
@@ -0,0 +1,9 @@
+module GitHelpers
+ def random_git_name
+ "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+ end
+end
+
+RSpec.configure do |config|
+ config.include GitHelpers
+end
diff --git a/spec/support/git_http_helpers.rb b/spec/support/git_http_helpers.rb
new file mode 100644
index 00000000000..46b686fce94
--- /dev/null
+++ b/spec/support/git_http_helpers.rb
@@ -0,0 +1,48 @@
+module GitHttpHelpers
+ def clone_get(project, options = {})
+ get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def clone_post(project, options = {})
+ post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def push_get(project, options = {})
+ get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def push_post(project, options = {})
+ post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def download(project, user: nil, password: nil, spnego_request_token: nil)
+ args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+
+ clone_get(*args)
+ yield response
+
+ clone_post(*args)
+ yield response
+ end
+
+ def upload(project, user: nil, password: nil, spnego_request_token: nil)
+ args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+
+ push_get(*args)
+ yield response
+
+ push_post(*args)
+ yield response
+ end
+
+ def auth_env(user, password, spnego_request_token)
+ env = workhorse_internal_api_request_header
+ if user && password
+ env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
+ elsif spnego_request_token
+ env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
+ end
+
+ env
+ end
+end
diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb
new file mode 100644
index 00000000000..f752508d48c
--- /dev/null
+++ b/spec/support/import_export/configuration_helper.rb
@@ -0,0 +1,29 @@
+module ConfigurationHelper
+ # Returns a list of models from hashes/arrays contained in +project_tree+
+ def names_from_tree(project_tree)
+ project_tree.map do |branch_or_model|
+ branch_or_model = branch_or_model.to_s if branch_or_model.is_a?(Symbol)
+
+ branch_or_model.is_a?(String) ? branch_or_model : names_from_tree(branch_or_model)
+ end
+ end
+
+ def relation_class_for_name(relation_name)
+ relation_name = Gitlab::ImportExport::RelationFactory::OVERRIDES[relation_name.to_sym] || relation_name
+ relation_name.to_s.classify.constantize
+ end
+
+ def parsed_attributes(relation_name, attributes)
+ excluded_attributes = config_hash['excluded_attributes'][relation_name]
+ included_attributes = config_hash['included_attributes'][relation_name]
+
+ attributes = attributes - JSON[excluded_attributes.to_json] if excluded_attributes
+ attributes = attributes & JSON[included_attributes.to_json] if included_attributes
+
+ attributes
+ end
+
+ def associations_for(safe_model)
+ safe_model.reflect_on_all_associations.map { |assoc| assoc.name.to_s }
+ end
+end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
new file mode 100644
index 00000000000..be0772d6a4a
--- /dev/null
+++ b/spec/support/import_export/export_file_helper.rb
@@ -0,0 +1,133 @@
+require './spec/support/import_export/configuration_helper'
+
+module ExportFileHelper
+ include ConfigurationHelper
+
+ ObjectWithParent = Struct.new(:object, :parent, :key_found)
+
+ def setup_project
+ project = create(:project, :public)
+
+ create(:release, project: project)
+
+ issue = create(:issue, assignee: user, project: project)
+ snippet = create(:project_snippet, project: project)
+ label = create(:label, project: project)
+ milestone = create(:milestone, project: project)
+ merge_request = create(:merge_request, source_project: project, milestone: milestone)
+ commit_status = create(:commit_status, project: project)
+
+ create(:label_link, label: label, target: issue)
+
+ ci_pipeline = create(:ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ statuses: [commit_status])
+
+ create(:ci_build, pipeline: ci_pipeline, project: project)
+ create(:milestone, project: project)
+ create(:note, noteable: issue, project: project)
+ create(:note, noteable: merge_request, project: project)
+ create(:note, noteable: snippet, project: project)
+ create(:note_on_commit,
+ author: user,
+ project: project,
+ commit_id: ci_pipeline.sha)
+
+ create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+ create(:project_member, :master, user: user, project: project)
+ create(:ci_variable, project: project)
+ create(:ci_trigger, project: project)
+ key = create(:deploy_key)
+ key.projects << project
+ create(:service, project: project)
+ create(:project_hook, project: project, token: 'token')
+ create(:protected_branch, project: project)
+
+ project
+ end
+
+ # Expands the compressed file for an exported project into +tmpdir+
+ def in_directory_with_expanded_export(project)
+ Dir.mktmpdir do |tmpdir|
+ export_file = project.export_project_path
+ _output, exit_status = Gitlab::Popen.popen(%W{tar -zxf #{export_file} -C #{tmpdir}})
+
+ yield(exit_status, tmpdir)
+ end
+ end
+
+ # Recursively finds key/values including +key+ as part of the key, inside a nested hash
+ def deep_find_with_parent(sensitive_key_word, object, found = nil)
+ sensitive_key_found = object_contains_key?(object, sensitive_key_word)
+
+ # Returns the parent object and the object found containing a sensitive word as part of the key
+ if sensitive_key_found && object[sensitive_key_found]
+ ObjectWithParent.new(object[sensitive_key_found], object, sensitive_key_found)
+ elsif object.is_a?(Enumerable)
+ # Recursively lookup for keys containing sensitive words in a Hash or Array
+ object_with_parent = nil
+
+ object.find do |*hash_or_array|
+ object_with_parent = deep_find_with_parent(sensitive_key_word, hash_or_array.last, found)
+ end
+
+ object_with_parent
+ end
+ end
+
+ # Return true if the hash has a key containing a sensitive word
+ def object_contains_key?(object, sensitive_key_word)
+ return false unless object.is_a?(Hash)
+
+ object.keys.find { |key| key.include?(sensitive_key_word) }
+ end
+
+ # Returns the offended ObjectWithParent object if a sensitive word is found inside a hash,
+ # excluding the whitelisted safe hashes.
+ def find_sensitive_attributes(sensitive_word, project_hash)
+ loop do
+ object_with_parent = deep_find_with_parent(sensitive_word, project_hash)
+
+ return nil unless object_with_parent && object_with_parent.object
+
+ if is_safe_hash?(object_with_parent.parent, sensitive_word)
+ # It's in the safe list, remove hash and keep looking
+ object_with_parent.parent.delete(object_with_parent.key_found)
+ else
+ return object_with_parent
+ end
+
+ nil
+ end
+ end
+
+ # Returns true if it's one of the excluded models in +safe_list+
+ def is_safe_hash?(parent, sensitive_word)
+ return false unless parent && safe_list[sensitive_word.to_sym]
+
+ # Extra attributes that appear in a model but not in the exported hash.
+ excluded_attributes = ['type']
+
+ safe_list[sensitive_word.to_sym].each do |model|
+ # Check whether this is a hash attribute inside a model
+ if model.is_a?(Symbol)
+ return true if (safe_hashes[model] - parent.keys).empty?
+ else
+ return true if safe_model?(model, excluded_attributes, parent)
+ end
+ end
+
+ false
+ end
+
+ # Compares model attributes with those those found in the hash
+ # and returns true if there is a match, ignoring some excluded attributes.
+ def safe_model?(model, excluded_attributes, parent)
+ excluded_attributes += associations_for(model)
+ parsed_model_attributes = parsed_attributes(model.name.underscore, model.attribute_names)
+
+ (parsed_model_attributes - parent.keys - excluded_attributes).empty?
+ end
+end
diff --git a/spec/support/ldap_helpers.rb b/spec/support/ldap_helpers.rb
new file mode 100644
index 00000000000..079f244475c
--- /dev/null
+++ b/spec/support/ldap_helpers.rb
@@ -0,0 +1,47 @@
+module LdapHelpers
+ def ldap_adapter(provider = 'ldapmain', ldap = double(:ldap))
+ ::Gitlab::LDAP::Adapter.new(provider, ldap)
+ end
+
+ def user_dn(uid)
+ "uid=#{uid},ou=users,dc=example,dc=com"
+ end
+
+ # Accepts a hash of Gitlab::LDAP::Config keys and values.
+ #
+ # Example:
+ # stub_ldap_config(
+ # group_base: 'ou=groups,dc=example,dc=com',
+ # admin_group: 'my-admin-group'
+ # )
+ def stub_ldap_config(messages)
+ messages.each do |config, value|
+ allow_any_instance_of(::Gitlab::LDAP::Config)
+ .to receive(config.to_sym).and_return(value)
+ end
+ end
+
+ # Stub an LDAP person search and provide the return entry. Specify `nil` for
+ # `entry` to simulate when an LDAP person is not found
+ #
+ # Example:
+ # adapter = ::Gitlab::LDAP::Adapter.new('ldapmain', double(:ldap))
+ # ldap_user_entry = ldap_user_entry('john_doe')
+ #
+ # stub_ldap_person_find_by_uid('john_doe', ldap_user_entry, adapter)
+ def stub_ldap_person_find_by_uid(uid, entry, provider = 'ldapmain')
+ return_value = ::Gitlab::LDAP::Person.new(entry, provider) if entry.present?
+
+ allow(::Gitlab::LDAP::Person)
+ .to receive(:find_by_uid).with(uid, any_args).and_return(return_value)
+ end
+
+ # Create a simple LDAP user entry.
+ def ldap_user_entry(uid)
+ entry = Net::LDAP::Entry.new
+ entry['dn'] = user_dn(uid)
+ entry['uid'] = uid
+
+ entry
+ end
+end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index e5f76afbfc0..c0b3e83244d 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -75,6 +75,7 @@ module LoginHelpers
def logout
find(".header-user-dropdown-toggle").click
click_link "Sign out"
+ expect(page).to have_content('Signed out successfully')
end
# Logout without JavaScript driver
diff --git a/spec/support/matchers/have_issuable_counts.rb b/spec/support/matchers/have_issuable_counts.rb
new file mode 100644
index 00000000000..02605d6b70e
--- /dev/null
+++ b/spec/support/matchers/have_issuable_counts.rb
@@ -0,0 +1,21 @@
+RSpec::Matchers.define :have_issuable_counts do |opts|
+ match do |actual|
+ expected_counts = opts.map do |state, count|
+ "#{state.to_s.humanize} #{count}"
+ end
+
+ actual.within '.issues-state-filters' do
+ expected_counts.each do |expected_count|
+ expect(actual).to have_content(expected_count)
+ end
+ end
+ end
+
+ description do
+ "displays the following issuable counts: #{expected_counts.inspect}"
+ end
+
+ failure_message do
+ "expected the following issuable counts: #{expected_counts.inspect} to be displayed"
+ end
+end
diff --git a/spec/support/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
index 5f9645ed44f..5f9645ed44f 100644
--- a/spec/support/issuable_create_service_slash_commands_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
diff --git a/spec/support/slash_commands_helpers.rb b/spec/support/slash_commands_helpers.rb
new file mode 100644
index 00000000000..df483afa0e3
--- /dev/null
+++ b/spec/support/slash_commands_helpers.rb
@@ -0,0 +1,10 @@
+module SlashCommandsHelpers
+ def write_note(text)
+ Sidekiq::Testing.fake! do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: text
+ click_button 'Comment'
+ end
+ end
+ end
+end
diff --git a/spec/support/snippets_shared_examples.rb b/spec/support/snippets_shared_examples.rb
new file mode 100644
index 00000000000..57dfff3471f
--- /dev/null
+++ b/spec/support/snippets_shared_examples.rb
@@ -0,0 +1,18 @@
+# These shared examples expect a `snippets` array of snippets
+RSpec.shared_examples 'paginated snippets' do |remote: false|
+ it "is limited to #{Snippet.default_per_page} items per page" do
+ expect(page.all('.snippets-list-holder .snippet-row').count).to eq(Snippet.default_per_page)
+ end
+
+ context 'clicking on the link to the second page' do
+ before do
+ click_link('2')
+ wait_for_ajax if remote
+ end
+
+ it 'shows the remaining snippets' do
+ remaining_snippets_count = [snippets.size - Snippet.default_per_page, Snippet.default_per_page].min
+ expect(page).to have_selector('.snippets-list-holder .snippet-row', count: remaining_snippets_count)
+ end
+ end
+end
diff --git a/spec/support/wait_for_vue_resource.rb b/spec/support/wait_for_vue_resource.rb
new file mode 100644
index 00000000000..1029f84716f
--- /dev/null
+++ b/spec/support/wait_for_vue_resource.rb
@@ -0,0 +1,7 @@
+module WaitForVueResource
+ def wait_for_vue_resource(spinner: true)
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until page.evaluate_script('Vue.activeResources').zero?
+ end
+ end
+end
diff --git a/spec/support/workhorse_helpers.rb b/spec/support/workhorse_helpers.rb
index 107b6e30924..47673cd4c3a 100644
--- a/spec/support/workhorse_helpers.rb
+++ b/spec/support/workhorse_helpers.rb
@@ -13,4 +13,9 @@ module WorkhorseHelpers
]
end
end
+
+ def workhorse_internal_api_request_header
+ jwt_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256')
+ { 'HTTP_' + Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER.upcase.tr('-', '_') => jwt_token }
+ end
end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index dae858a52f6..68d2d72876e 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'admin/dashboard/index.html.haml' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
before do
assign(:projects, create_list(:empty_project, 1))
diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb
new file mode 100644
index 00000000000..793b747e7eb
--- /dev/null
+++ b/spec/views/ci/lints/show.html.haml_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe 'ci/lints/show' do
+ let(:content) do
+ {
+ build_template: {
+ script: './build.sh',
+ tags: ['dotnet'],
+ only: ['test@dude/repo'],
+ except: ['deploy'],
+ environment: 'testing'
+ }
+ }
+ end
+
+ let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
+
+ context 'when the content is valid' do
+ before do
+ assign(:status, true)
+ assign(:builds, config_processor.builds)
+ assign(:stages, config_processor.stages)
+ assign(:jobs, config_processor.jobs)
+ end
+
+ it 'shows the correct values' do
+ render
+
+ expect(rendered).to have_content('Tag list: dotnet')
+ expect(rendered).to have_content('Refs only: test@dude/repo')
+ expect(rendered).to have_content('Refs except: deploy')
+ expect(rendered).to have_content('Environment: testing')
+ expect(rendered).to have_content('When: on_success')
+ end
+ end
+
+ context 'when the content is invalid' do
+ before do
+ assign(:status, false)
+ assign(:error, 'Undefined error')
+ end
+
+ it 'shows error message' do
+ render
+
+ expect(rendered).to have_content('Status: syntax is incorrect')
+ expect(rendered).to have_content('Error: Undefined error')
+ expect(rendered).not_to have_content('Tag list:')
+ end
+ end
+end
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
index 464051063d8..da43622d3f9 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/builds/show' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:project) { create(:project) }
let(:pipeline) do
@@ -59,14 +59,10 @@ describe 'projects/builds/show' do
end
it 'shows trigger variables in separate lines' do
- expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_1', 'TRIGGER_VALUE_1'))
- expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_2', 'TRIGGER_VALUE_2'))
+ expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_1')
+ expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_2')
+ expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_1')
+ expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2')
end
end
-
- private
-
- def variable_regexp(key, value)
- /\A#{Regexp.escape("#{key}=#{value}")}\Z/
- end
end
diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
index 78af61f15a7..c8a3d02d8fd 100644
--- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/issues/_related_branches' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:project) { create(:project) }
let(:branch) { project.repository.find_branch('feature') }
diff --git a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
index 733b2dfa7ff..86980f59cd8 100644
--- a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/merge_requests/widget/_heading' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
context 'when released to an environment' do
let(:project) { merge_request.target_project }
@@ -15,6 +15,8 @@ describe 'projects/merge_requests/widget/_heading' do
assign(:merge_request, merge_request)
assign(:project, project)
+ allow(view).to receive(:can?).and_return(true)
+
render
end
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
index 9da05e1721c..3650b22c389 100644
--- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/merge_requests/edit.html.haml' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index fe0780e72df..33cabd14913 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/merge_requests/show.html.haml' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
let(:project) { create(:project) }
@@ -41,4 +41,17 @@ describe 'projects/merge_requests/show.html.haml' do
expect(rendered).to have_css('a', visible: false, text: 'Close')
end
end
+
+ context 'when the merge request is open' do
+ it 'closes the merge request if the source project does not exist' do
+ closed_merge_request.update_attributes(state: 'open')
+ fork_project.destroy
+
+ render
+
+ expect(closed_merge_request.reload.state).to eq('closed')
+ expect(rendered).to have_css('a', visible: false, text: 'Reopen')
+ expect(rendered).to have_css('a', visible: false, text: 'Close')
+ end
+ end
end
diff --git a/spec/views/projects/notes/_form.html.haml_spec.rb b/spec/views/projects/notes/_form.html.haml_spec.rb
new file mode 100644
index 00000000000..b14b1ece2d0
--- /dev/null
+++ b/spec/views/projects/notes/_form.html.haml_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe 'projects/notes/_form' do
+ include Devise::Test::ControllerHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.team << [user, :master]
+ assign(:project, project)
+ assign(:note, note)
+
+ allow(view).to receive(:current_user).and_return(user)
+
+ render
+ end
+
+ %w[issue merge_request].each do |noteable|
+ context "with a note on #{noteable}" do
+ let(:note) { build(:"note_on_#{noteable}", project: project) }
+
+ it 'says that only markdown is supported, not slash commands' do
+ expect(rendered).to have_content('Styling with Markdown and slash commands are supported')
+ end
+ end
+ end
+
+ context 'with a note on a commit' do
+ let(:note) { build(:note_on_commit, project: project) }
+
+ it 'says that only markdown is supported, not slash commands' do
+ expect(rendered).to have_content('Styling with Markdown is supported')
+ end
+ end
+end
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
new file mode 100644
index 00000000000..bf027499c94
--- /dev/null
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe 'projects/pipelines/show' do
+ include Devise::Test::ControllerHelpers
+
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
+
+ before do
+ controller.prepend_view_path('app/views/projects')
+
+ create_build('build', 0, 'build', :success)
+ create_build('test', 1, 'rspec 0:2', :pending)
+ create_build('test', 1, 'rspec 1:2', :running)
+ create_build('test', 1, 'spinach 0:2', :created)
+ create_build('test', 1, 'spinach 1:2', :created)
+ create_build('test', 1, 'audit', :created)
+ create_build('deploy', 2, 'production', :created)
+
+ create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3)
+
+ assign(:project, project)
+ assign(:pipeline, pipeline)
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ it 'shows a graph with grouped stages' do
+ render
+
+ expect(rendered).to have_css('.pipeline-graph')
+ expect(rendered).to have_css('.grouped-pipeline-dropdown')
+
+ # stages
+ expect(rendered).to have_text('Build')
+ expect(rendered).to have_text('Test')
+ expect(rendered).to have_text('Deploy')
+ expect(rendered).to have_text('External')
+
+ # builds
+ expect(rendered).to have_text('rspec')
+ expect(rendered).to have_text('spinach')
+ expect(rendered).to have_text('rspec 0:2')
+ expect(rendered).to have_text('production')
+ expect(rendered).to have_text('jenkins')
+ end
+
+ private
+
+ def create_build(stage, stage_idx, name, status)
+ create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name, status: status)
+ end
+end
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 0f3fc1ee1ac..c381b1a86df 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/tree/show' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:project) { create(:project) }
let(:repository) { project.repository }
diff --git a/spec/workers/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb
new file mode 100644
index 00000000000..35e1518a35e
--- /dev/null
+++ b/spec/workers/prune_old_events_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe PruneOldEventsWorker do
+ describe '#perform' do
+ let!(:expired_event) { create(:event, author_id: 0, created_at: 13.months.ago) }
+ let!(:not_expired_event) { create(:event, author_id: 0, created_at: 1.day.ago) }
+ let!(:exactly_12_months_event) { create(:event, author_id: 0, created_at: 12.months.ago) }
+
+ it 'prunes events older than 12 months' do
+ expect { subject.perform }.to change { Event.count }.by(-1)
+ expect(Event.find_by(id: expired_event.id)).to be_nil
+ end
+
+ it 'leaves fresh events' do
+ subject.perform
+ expect(not_expired_event.reload).to be_present
+ end
+
+ it 'leaves events from exactly 12 months ago' do
+ subject.perform
+ expect(exactly_12_months_event).to be_present
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/task_list.js b/vendor/assets/javascripts/task_list.js
index bc451506b6a..9fbfef03f6d 100644
--- a/vendor/assets/javascripts/task_list.js
+++ b/vendor/assets/javascripts/task_list.js
@@ -1,15 +1,118 @@
-
+// The MIT License (MIT)
+//
+// Copyright (c) 2014 GitHub, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+// TaskList Behavior
+//
/*= provides tasklist:enabled */
-
-
/*= provides tasklist:disabled */
-
-
/*= provides tasklist:change */
-
-
/*= provides tasklist:changed */
-
+//
+//
+// Enables Task List update behavior.
+//
+// ### Example Markup
+//
+// <div class="js-task-list-container">
+// <ul class="task-list">
+// <li class="task-list-item">
+// <input type="checkbox" class="js-task-list-item-checkbox" disabled />
+// text
+// </li>
+// </ul>
+// <form>
+// <textarea class="js-task-list-field">- [ ] text</textarea>
+// </form>
+// </div>
+//
+// ### Specification
+//
+// TaskLists MUST be contained in a `(div).js-task-list-container`.
+//
+// TaskList Items SHOULD be an a list (`UL`/`OL`) element.
+//
+// Task list items MUST match `(input).task-list-item-checkbox` and MUST be
+// `disabled` by default.
+//
+// TaskLists MUST have a `(textarea).js-task-list-field` form element whose
+// `value` attribute is the source (Markdown) to be udpated. The source MUST
+// follow the syntax guidelines.
+//
+// TaskList updates trigger `tasklist:change` events. If the change is
+// successful, `tasklist:changed` is fired. The change can be canceled.
+//
+// jQuery is required.
+//
+// ### Methods
+//
+// `.taskList('enable')` or `.taskList()`
+//
+// Enables TaskList updates for the container.
+//
+// `.taskList('disable')`
+//
+// Disables TaskList updates for the container.
+//
+//# ### Events
+//
+// `tasklist:enabled`
+//
+// Fired when the TaskList is enabled.
+//
+// * **Synchronicity** Sync
+// * **Bubbles** Yes
+// * **Cancelable** No
+// * **Target** `.js-task-list-container`
+//
+// `tasklist:disabled`
+//
+// Fired when the TaskList is disabled.
+//
+// * **Synchronicity** Sync
+// * **Bubbles** Yes
+// * **Cancelable** No
+// * **Target** `.js-task-list-container`
+//
+// `tasklist:change`
+//
+// Fired before the TaskList item change takes affect.
+//
+// * **Synchronicity** Sync
+// * **Bubbles** Yes
+// * **Cancelable** Yes
+// * **Target** `.js-task-list-field`
+//
+// `tasklist:changed`
+//
+// Fired once the TaskList item change has taken affect.
+//
+// * **Synchronicity** Sync
+// * **Bubbles** Yes
+// * **Cancelable** No
+// * **Target** `.js-task-list-field`
+//
+// ### NOTE
+//
+// Task list checkboxes are rendered as disabled by default because rendered
+// user content is cached without regard for the viewer.
(function() {
var codeFencesPattern, complete, completePattern, disableTaskList, disableTaskLists, enableTaskList, enableTaskLists, escapePattern, incomplete, incompletePattern, itemPattern, itemsInParasPattern, updateTaskList, updateTaskListItem,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
@@ -18,20 +121,48 @@
complete = "[x]";
+ // Escapes the String for regular expression matching.
escapePattern = function(str) {
return str.replace(/([\[\]])/g, "\\$1").replace(/\s/, "\\s").replace("x", "[xX]");
};
- incompletePattern = RegExp("" + (escapePattern(incomplete)));
-
- completePattern = RegExp("" + (escapePattern(complete)));
+ incompletePattern = RegExp("" + (escapePattern(incomplete))); // escape square brackets
+ // match all white space
+ completePattern = RegExp("" + (escapePattern(complete))); // match all cases
+ // Pattern used to identify all task list items.
+ // Useful when you need iterate over all items.
itemPattern = RegExp("^(?:\\s*(?:>\\s*)*(?:[-+*]|(?:\\d+\\.)))\\s*(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ")\\s+(?!\\(.*?\\))(?=(?:\\[.*?\\]\\s*(?:\\[.*?\\]|\\(.*?\\))\\s*)*(?:[^\\[]|$))");
+ // prefix, consisting of
+ // optional leading whitespace
+ // zero or more blockquotes
+ // list item indicator
+ // optional whitespace prefix
+ // checkbox
+ // is followed by whitespace
+ // is not part of a [foo](url) link
+ // and is followed by zero or more links
+ // and either a non-link or the end of the string
+ // Used to filter out code fences from the source for comparison only.
+ // http://rubular.com/r/x5EwZVrloI
+ // Modified slightly due to issues with JS
codeFencesPattern = /^`{3}(?:\s*\w+)?[\S\s].*[\S\s]^`{3}$/mg;
+ // ```
+ // followed by optional language
+ // whitespace
+ // code
+ // whitespace
+ // ```
+ // Used to filter out potential mismatches (items not in lists).
+ // http://rubular.com/r/OInl6CiePy
itemsInParasPattern = RegExp("^(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ").+$", "g");
+ // Given the source text, updates the appropriate task list item to match the
+ // given checked value.
+ //
+ // Returns the updated String text.
updateTaskListItem = function(source, itemIndex, checked) {
var clean, index, line, result;
clean = source.replace(/\r/g, '').replace(codeFencesPattern, '').replace(itemsInParasPattern, '').split("\n");
@@ -55,6 +186,9 @@
return result.join("\n");
};
+ // Updates the $field value to reflect the state of $item.
+ // Triggers the `tasklist:change` event before the value has changed, and fires
+ // a `tasklist:changed` event once the value has changed.
updateTaskList = function($item) {
var $container, $field, checked, event, index;
$container = $item.closest('.js-task-list-container');
@@ -70,10 +204,12 @@
}
};
+ // When the task list item checkbox is updated, submit the change
$(document).on('change', '.task-list-item-checkbox', function() {
return updateTaskList($(this));
});
+ // Enables TaskList item changes.
enableTaskList = function($container) {
if ($container.find('.js-task-list-field').length > 0) {
$container.find('.task-list-item').addClass('enabled').find('.task-list-item-checkbox').attr('disabled', null);
@@ -81,6 +217,7 @@
}
};
+ // Enables a collection of TaskList containers.
enableTaskLists = function($containers) {
var container, i, len, results;
results = [];
@@ -91,11 +228,13 @@
return results;
};
+ // Disable TaskList item changes.
disableTaskList = function($container) {
$container.find('.task-list-item').removeClass('enabled').find('.task-list-item-checkbox').attr('disabled', 'disabled');
return $container.removeClass('is-task-list-enabled').trigger('tasklist:disabled');
};
+ // Disables a collection of TaskList containers.
disableTaskLists = function($containers) {
var container, i, len, results;
results = [];
diff --git a/vendor/gitignore/Erlang.gitignore b/vendor/gitignore/Erlang.gitignore
index 8e46d5a07f8..3826c85736f 100644
--- a/vendor/gitignore/Erlang.gitignore
+++ b/vendor/gitignore/Erlang.gitignore
@@ -4,7 +4,7 @@ deps
*.beam
*.plt
erl_crash.dump
-ebin
+ebin/*.beam
rel/example_project
.concrete/DEV_MODE
.rebar
diff --git a/vendor/gitignore/Global/Ansible.gitignore b/vendor/gitignore/Global/Ansible.gitignore
new file mode 100644
index 00000000000..a8b42eb6eed
--- /dev/null
+++ b/vendor/gitignore/Global/Ansible.gitignore
@@ -0,0 +1 @@
+*.retry
diff --git a/vendor/gitignore/Global/Linux.gitignore b/vendor/gitignore/Global/Linux.gitignore
index cc9586893b6..b56bf65d855 100644
--- a/vendor/gitignore/Global/Linux.gitignore
+++ b/vendor/gitignore/Global/Linux.gitignore
@@ -8,3 +8,6 @@
# Linux trash folder which might appear on any partition or disk
.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
diff --git a/vendor/gitignore/Global/NetBeans.gitignore b/vendor/gitignore/Global/NetBeans.gitignore
index 520d91ff584..254108cd23b 100644
--- a/vendor/gitignore/Global/NetBeans.gitignore
+++ b/vendor/gitignore/Global/NetBeans.gitignore
@@ -3,5 +3,4 @@ build/
nbbuild/
dist/
nbdist/
-nbactions.xml
.nb-gradle/
diff --git a/vendor/gitignore/Global/Tags.gitignore b/vendor/gitignore/Global/Tags.gitignore
index c0318165a27..91927af4cd6 100644
--- a/vendor/gitignore/Global/Tags.gitignore
+++ b/vendor/gitignore/Global/Tags.gitignore
@@ -9,6 +9,7 @@ gtags.files
GTAGS
GRTAGS
GPATH
+GSYMS
cscope.files
cscope.out
cscope.in.out
diff --git a/vendor/gitignore/Global/OSX.gitignore b/vendor/gitignore/Global/macOS.gitignore
index 5972fe50f66..828a509a137 100644
--- a/vendor/gitignore/Global/OSX.gitignore
+++ b/vendor/gitignore/Global/macOS.gitignore
@@ -3,7 +3,8 @@
.LSOverride
# Icon must end with two \r
-Icon
+Icon
+
# Thumbnails
._*
diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore
index cd0d5d1e2f4..397a0ed4acb 100644
--- a/vendor/gitignore/Go.gitignore
+++ b/vendor/gitignore/Go.gitignore
@@ -25,3 +25,6 @@ _testmain.go
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
+
+# external packages folder
+vendor/
diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore
index a4ee41ab62b..450f32ec40c 100644
--- a/vendor/gitignore/Haskell.gitignore
+++ b/vendor/gitignore/Haskell.gitignore
@@ -17,3 +17,4 @@ cabal.sandbox.config
*.eventlog
.stack-work/
cabal.project.local
+.HTF/
diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore
index 0d7a0de298f..93103fdbe77 100644
--- a/vendor/gitignore/Joomla.gitignore
+++ b/vendor/gitignore/Joomla.gitignore
@@ -52,6 +52,7 @@
/administrator/language/en-GB/en-GB.plg_content_contact.sys.ini
/administrator/language/en-GB/en-GB.plg_content_finder.ini
/administrator/language/en-GB/en-GB.plg_content_finder.sys.ini
+/administrator/language/en-GB/en-GB.plg_editors-xtd_module*
/administrator/language/en-GB/en-GB.plg_finder_categories.ini
/administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini
/administrator/language/en-GB/en-GB.plg_finder_contacts.ini
@@ -64,6 +65,10 @@
/administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini
/administrator/language/en-GB/en-GB.plg_finder_weblinks.ini
/administrator/language/en-GB/en-GB.plg_finder_weblinks.sys.ini
+/administrator/language/en-GB/en-GB.plg_installer_folderinstaller*
+/administrator/language/en-GB/en-GB.plg_installer_packageinstaller*
+/administrator/language/en-GB/en-GB.plg_installer_packageinstaller
+/administrator/language/en-GB/en-GB.plg_installer_urlinstaller*
/administrator/language/en-GB/en-GB.plg_installer_webinstaller.ini
/administrator/language/en-GB/en-GB.plg_installer_webinstaller.sys.ini
/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini
@@ -72,6 +77,8 @@
/administrator/language/en-GB/en-GB.plg_search_tags.sys.ini
/administrator/language/en-GB/en-GB.plg_system_languagecode.ini
/administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_stats*
+/administrator/language/en-GB/en-GB.plg_system_updatenotification*
/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini
/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini
/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini
@@ -249,8 +256,10 @@
/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini
/administrator/language/en-GB/en-GB.xml
/administrator/language/en-GB/index.html
+/administrator/language/ru-RU/index.html
/administrator/language/overrides/*
/administrator/language/index.html
+/administrator/logs/index.html
/administrator/manifests/*
/administrator/modules/mod_custom/*
/administrator/modules/mod_feed/*
@@ -289,6 +298,7 @@
/components/com_finder/*
/components/com_mailto/*
/components/com_media/*
+/components/com_modules/*
/components/com_newsfeeds/*
/components/com_search/*
/components/com_users/*
@@ -407,6 +417,7 @@
/libraries/idna_convert/*
/libraries/joomla/*
/libraries/legacy/*
+/libraries/php-encryption/*
/libraries/phpass/*
/libraries/phpmailer/*
/libraries/phputf8/*
@@ -431,9 +442,11 @@
/media/media/*
/media/mod_languages/*
/media/overrider/*
+/media/plg_captcha_recaptcha/*
/media/plg_quickicon_extensionupdate/*
/media/plg_quickicon_joomlaupdate/*
/media/plg_system_highlight/*
+/media/plg_system_stats/*
/media/system/*
/media/index.html
/modules/mod_articles_archive/*
@@ -486,6 +499,7 @@
/plugins/editors/none/*
/plugins/editors/tinymce/*
/plugins/editors/index.html
+/plugins/editors-xtd/module/*
/plugins/editors-xtd/article/*
/plugins/editors-xtd/image/*
/plugins/editors-xtd/pagebreak/*
@@ -523,6 +537,8 @@
/plugins/system/redirect/*
/plugins/system/remember/*
/plugins/system/sef/*
+/plugins/system/stats/*
+/plugins/system/updatenotification/*
/plugins/system/index.html
/plugins/twofactorauth/*
/plugins/user/contactcreator/*
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index aea5294de9d..bc7fc55724c 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -34,5 +34,11 @@ jspm_packages
# Optional npm cache directory
.npm
+# Optional eslint cache
+.eslintcache
+
# Optional REPL history
.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 20592083931..58c51ecaed4 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -50,7 +50,9 @@ Carthage/Build
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
fastlane/report.xml
+fastlane/Preview.html
fastlane/screenshots
+fastlane/test_output
# Code Injection
#
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 72364f99fe4..37fc9d40817 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -79,6 +79,7 @@ celerybeat-schedule
.env
# virtualenv
+.venv/
venv/
ENV/
diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore
index d8c256c1925..e97427608c1 100644
--- a/vendor/gitignore/Rails.gitignore
+++ b/vendor/gitignore/Rails.gitignore
@@ -12,9 +12,11 @@ capybara-*.html
rerun.txt
pickle-email-*.html
-# TODO Comment out these rules if you are OK with secrets being uploaded to the repo
+# TODO Comment out this rule if you are OK with secrets being uploaded to the repo
config/initializers/secret_token.rb
-config/secrets.yml
+
+# Only include if you have production secrets in this file, which is no longer a Rails default
+# config/secrets.yml
# dotenv
# TODO Comment out this rule if environment variables can be committed
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 34f999df3e7..f620fad23eb 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -192,3 +192,6 @@ TSWLatexianTemp*
# KBibTeX
*~[0-9]*
+
+# auto folder when using emacs and auctex
+/auto/*
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 67acbf42f5e..1b86e7ec918 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -110,6 +110,10 @@ _TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
# NCrunch
_NCrunch_*
.*crunch*.local.xml
@@ -189,6 +193,7 @@ ClientBin/
*~
*.dbmdl
*.dbproj.schemaview
+*.jfm
*.pfx
*.publishsettings
node_modules/
@@ -251,3 +256,13 @@ paket-files/
# JetBrains Rider
.idea/
*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/
diff --git a/vendor/gitlab-ci-yml/.gitlab-ci.yml b/vendor/gitlab-ci-yml/.gitlab-ci.yml
new file mode 100644
index 00000000000..18b14554887
--- /dev/null
+++ b/vendor/gitlab-ci-yml/.gitlab-ci.yml
@@ -0,0 +1,4 @@
+image: ruby:2.3-alpine
+
+test:
+ script: ruby verify_templates.rb
diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
index 396d3f1b042..f3fa3949656 100644
--- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
@@ -1,7 +1,12 @@
# Official docker image.
image: docker:latest
+services:
+ - docker:dind
+
build:
stage: build
script:
- - docker build -t test .
+ - docker login -u "gitlab-ci-token" -p "$CI_BUILD_TOKEN" $CI_REGISTRY
+ - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME" .
+ - docker push "$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME"
diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
new file mode 100644
index 00000000000..263c4c19999
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
@@ -0,0 +1,34 @@
+# This template uses the java:8 docker image because there isn't any
+# official Gradle image at this moment
+#
+# This is the Gradle build system for JVM applications
+# https://gradle.org/
+# https://github.com/gradle/gradle
+image: java:8
+
+# Make the gradle wrapper executable. This essentially downloads a copy of
+# Gradle to build the project with.
+# https://docs.gradle.org/current/userguide/gradle_wrapper.html
+# It is expected that any modern gradle project has a wrapper
+before_script:
+ - chmod +x gradlew
+
+# We redirect the gradle user home using -g so that it caches the
+# wrapper and dependencies.
+# https://docs.gradle.org/current/userguide/gradle_command_line.html
+#
+# Unfortunately it also caches the build output so
+# cleaning removes reminants of any cached builds.
+# The assemble task actually builds the project.
+# If it fails here, the tests can't run.
+build:
+ stage: build
+ script:
+ - ./gradlew -g /cache/.gradle clean assemble
+ allow_failure: false
+
+# Use the generated build output to run the tests.
+test:
+ stage: test
+ script:
+ - ./gradlew -g /cache./gradle check
diff --git a/vendor/gitlab-ci-yml/Julia.gitlab-ci.yml b/vendor/gitlab-ci-yml/Julia.gitlab-ci.yml
new file mode 100644
index 00000000000..140cb4635f3
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Julia.gitlab-ci.yml
@@ -0,0 +1,54 @@
+# An example .gitlab-ci.yml file to test (and optionally report the coverage
+# results of) your [Julia][1] packages. Please refer to the [documentation][2]
+# for more information about package development in Julia.
+#
+# Here, it is assumed that your Julia package is named `MyPackage`. Change it to
+# whatever name you have given to your package.
+#
+# [1]: http://julialang.org/
+# [2]: http://julia.readthedocs.org/
+
+# Below is the template to run your tests in Julia
+.test_template: &test_definition
+ # Uncomment below if you would like to run the tests on specific references
+ # only, such as the branches `master`, `development`, etc.
+ # only:
+ # - master
+ # - development
+ script:
+ # Let's run the tests. Substitute `coverage = false` below, if you do not
+ # want coverage results.
+ - /opt/julia/bin/julia -e 'Pkg.clone(pwd()); Pkg.test("MyPackage",
+ coverage = true)'
+ # Comment out below if you do not want coverage results.
+ - /opt/julia/bin/julia -e 'Pkg.add("Coverage"); cd(Pkg.dir("MyPackage"));
+ using Coverage; cl, tl = get_summary(process_folder());
+ println("(", cl/tl*100, "%) covered")'
+
+# Name a test and select an appropriate image.
+test:0.4.6:
+ image: julialang/julia:v0.4.6
+ <<: *test_definition
+
+# Maybe you would like to test your package against the development branch:
+test:0.5.0-dev:
+ image: julialang/julia:v0.5.0-dev
+ # ... allowing for failures, since we are testing against the development
+ # branch:
+ allow_failure: true
+ <<: *test_definition
+
+# REMARK: Do not forget to enable the coverage feature for your project, if you
+# are using code coverage reporting above. This can be done by
+#
+# - Navigating to the `CI/CD Pipelines` settings of your project,
+# - Copying and pasting the default `Simplecov` regex example provided, i.e.,
+# `\(\d+.\d+\%\) covered` in the `test coverage parsing` textfield.
+#
+# WARNING: This template is using the `julialang/julia` images from [Docker
+# Hub][3]. One can use custom Julia images and/or the official ones found
+# in the same place. However, care must be taken to correctly locate the binary
+# file (`/opt/julia/bin/julia` above), which is usually given on the image's
+# description page.
+#
+# [3]: http://hub.docker.com/
diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
index 166f146ee05..08b57c8c0ac 100644
--- a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
@@ -43,3 +43,12 @@ rails:
- bundle exec rake db:migrate
- bundle exec rake db:seed
- bundle exec rake test
+
+# This deploy job uses a simple deploy flow to Heroku, other providers, e.g. AWS Elastic Beanstalk
+# are supported too: https://github.com/travis-ci/dpl
+deploy:
+ type: deploy
+ environment: production
+ script:
+ - gem install dpl
+ - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_PRODUCTION_KEY
diff --git a/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml b/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml
new file mode 100644
index 00000000000..c9c35906d1c
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml
@@ -0,0 +1,30 @@
+# Lifted from: https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/
+# This file assumes an own GitLab CI runner, setup on an OS X system.
+stages:
+ - build
+ - archive
+
+build_project:
+ stage: build
+ script:
+ - xcodebuild clean -project ProjectName.xcodeproj -scheme SchemeName | xcpretty
+ - xcodebuild test -project ProjectName.xcodeproj -scheme SchemeName -destination 'platform=iOS Simulator,name=iPhone 6s,OS=9.2' | xcpretty -s
+ tags:
+ - ios_9-2
+ - xcode_7-2
+ - osx_10-11
+
+archive_project:
+ stage: archive
+ script:
+ - xcodebuild clean archive -archivePath build/ProjectName -scheme SchemeName
+ - xcodebuild -exportArchive -exportFormat ipa -archivePath "build/ProjectName.xcarchive" -exportPath "build/ProjectName.ipa" -exportProvisioningProfile "ProvisioningProfileName"
+ only:
+ - master
+ artifacts:
+ paths:
+ - build/ProjectName.ipa
+ tags:
+ - ios_9-2
+ - xcode_7-2
+ - osx_10-11